looper 1.7.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,293 @@
1
+ """
2
+ Argument definitions via a thin wrapper around `pydantic.fields.FieldInfo`
3
+ """
4
+
5
+ import enum
6
+ import os
7
+ from copy import copy
8
+ from typing import Any, List
9
+
10
+ import pydantic.v1 as pydantic
11
+
12
+
13
+ class Argument(pydantic.fields.FieldInfo):
14
+ """
15
+ CLI argument / flag definition
16
+
17
+ This class is designed to define CLI arguments or flags. It leverages
18
+ Pydantic for data validation and serves as a source of truth for multiple
19
+ interfaces, including a CLI.
20
+
21
+ Naively, one would think one could just subclass `pydantic.Field`,
22
+ but actually `pydantic.Field` is a function, and not a class.
23
+ `pydantic.Field()` returns a validated `FieldInfo` instance,
24
+ so we instead subclass `FieldInfo` directly and validate it in the
25
+ constructor.
26
+
27
+ :param str name: argument name, e.g. "ignore-args"
28
+ :param Any default: a tuple of the form (type, default_value). If the
29
+ default value is `...` (Ellipsis), then the argument is required.
30
+ :param str description: argument description, which will appear as the
31
+ help text for this argument
32
+ :param dict kwargs: additional keyword arguments supported by
33
+ `FieldInfo`. These are passed along as they are.
34
+ """
35
+
36
+ def __init__(
37
+ self, name: str, default: Any, description: str, alias: str = None, **kwargs
38
+ ) -> None:
39
+ self._name = name
40
+ super().__init__(
41
+ default=default, description=description, alias=alias, **kwargs
42
+ )
43
+ self._validate()
44
+
45
+ @property
46
+ def name(self):
47
+ """
48
+ Argument name as used in the CLI, e.g. "ignore-args"
49
+ """
50
+ return self._name
51
+
52
+ def with_reduced_default(self) -> pydantic.fields.FieldInfo:
53
+ """
54
+ Convert to a `FieldInfo` instance with reduced default value
55
+
56
+ Returns a copy of an instance, but with the `default` attribute
57
+ replaced by only the default value, without the type information.
58
+ This is required when using an instance in a direct `pydantic`
59
+ model definition, instead of creating a model dynamically using
60
+ `pydantic.create_model`.
61
+
62
+ TODO: this is due to this issue:
63
+ https://github.com/pydantic/pydantic/issues/2248#issuecomment-757448447
64
+ and it's a bit tedious.
65
+
66
+ """
67
+ c = copy(self)
68
+ _, default_value = self.default
69
+ c.default = default_value
70
+ return c
71
+
72
+
73
+ class ArgumentEnum(enum.Enum):
74
+ """
75
+ Lists all available arguments
76
+
77
+ Having a single "repository" of arguments allows us to re-use them easily across different commands.
78
+
79
+ TODO: not sure whether an enum is the ideal data structure for that
80
+ """
81
+
82
+ IGNORE_FLAGS = Argument(
83
+ name="ignore_flags",
84
+ alias="-i",
85
+ default=(bool, False),
86
+ description="Ignore run status flags",
87
+ )
88
+ FORCE_YES = Argument(
89
+ name="force_yes",
90
+ alias="-f",
91
+ default=(bool, False),
92
+ description="Provide upfront confirmation of destruction intent, to skip console query. Default=False",
93
+ )
94
+
95
+ DESCRIBE_CODES = Argument(
96
+ name="describe_codes",
97
+ default=(bool, False),
98
+ description="Show status codes description. Default=False",
99
+ )
100
+
101
+ ITEMIZED = Argument(
102
+ name="itemized",
103
+ default=(bool, False),
104
+ description="Show detailed overview of sample statuses. Default=False",
105
+ )
106
+
107
+ FLAGS = Argument(
108
+ name="flags",
109
+ alias="-f",
110
+ default=(List, []),
111
+ description="Only check samples based on these status flags.",
112
+ )
113
+
114
+ TIME_DELAY = Argument(
115
+ name="time_delay",
116
+ alias="-t",
117
+ default=(int, 0),
118
+ description="Time delay in seconds between job submissions (min: 0, max: 30)",
119
+ )
120
+ DRY_RUN = Argument(
121
+ name="dry_run",
122
+ alias="-d",
123
+ default=(bool, False),
124
+ description="Don't actually submit jobs",
125
+ )
126
+ COMMAND_EXTRA = Argument(
127
+ name="command_extra",
128
+ alias="-x",
129
+ default=(str, ""),
130
+ description="String to append to every command",
131
+ )
132
+ COMMAND_EXTRA_OVERRIDE = Argument(
133
+ name="command_extra_override",
134
+ alias="-y",
135
+ default=(str, ""),
136
+ description="Same as command-extra, but overrides values in PEP",
137
+ )
138
+ LUMP = Argument(
139
+ name="lump",
140
+ alias="-u",
141
+ default=(float, None),
142
+ description="Total input file size (GB) to batch into one job",
143
+ )
144
+ LUMPN = Argument(
145
+ name="lump_n",
146
+ alias="-n",
147
+ default=(int, None),
148
+ description="Number of commands to batch into one job",
149
+ )
150
+ LUMPJ = Argument(
151
+ name="lump_j",
152
+ alias="-j",
153
+ default=(int, None),
154
+ description="Lump samples into number of jobs.",
155
+ )
156
+ LIMIT = Argument(
157
+ name="limit", alias="-l", default=(int, None), description="Limit to n samples"
158
+ )
159
+ SKIP = Argument(
160
+ name="skip",
161
+ alias="-k",
162
+ default=(int, None),
163
+ description="Skip samples by numerical index",
164
+ )
165
+ CONFIG = Argument(
166
+ name="config",
167
+ alias="-c",
168
+ default=(str, None),
169
+ description="Looper configuration file (YAML)",
170
+ )
171
+ SETTINGS = Argument(
172
+ name="settings",
173
+ default=(str, ""),
174
+ description="Path to a YAML settings file with compute settings",
175
+ )
176
+ PEP_CONFIG = Argument(
177
+ name="pep_config",
178
+ default=(str, None),
179
+ description="PEP configuration file",
180
+ )
181
+ OUTPUT_DIR = Argument(
182
+ name="output_dir",
183
+ alias="-o",
184
+ default=(str, None),
185
+ description="Output directory",
186
+ )
187
+ REPORT_OUTPUT_DIR = Argument(
188
+ name="report_dir",
189
+ alias="-r",
190
+ default=(str, None),
191
+ description="Set location for looper report and looper table outputs",
192
+ )
193
+
194
+ GENERIC = Argument(
195
+ name="generic",
196
+ alias="-g",
197
+ default=(bool, False),
198
+ description="Use generic looper config?",
199
+ )
200
+
201
+ SAMPLE_PIPELINE_INTERFACES = Argument(
202
+ name="sample_pipeline_interfaces",
203
+ alias="-S",
204
+ default=(List, []),
205
+ description="Paths to looper sample pipeline interfaces",
206
+ )
207
+ PROJECT_PIPELINE_INTERFACES = Argument(
208
+ name="project_pipeline_interfaces",
209
+ alias="-P",
210
+ default=(List, []),
211
+ description="Paths to looper project pipeline interfaces",
212
+ )
213
+ AMEND = Argument(
214
+ name="amend", default=(List, []), description="List of amendments to activate"
215
+ )
216
+ SEL_ATTR = Argument(
217
+ name="sel_attr",
218
+ default=(str, "toggle"),
219
+ description="Attribute for sample exclusion OR inclusion",
220
+ )
221
+ SEL_INCL = Argument(
222
+ name="sel_incl",
223
+ default=(List, []),
224
+ description="Include only samples with these values",
225
+ )
226
+ SEL_EXCL = Argument(
227
+ name="sel_excl",
228
+ default=(str, ""),
229
+ description="Exclude samples with these values",
230
+ )
231
+ SEL_FLAG = Argument(
232
+ name="sel_flag", default=(List, []), description="Sample selection flag"
233
+ )
234
+ EXC_FLAG = Argument(
235
+ name="exc_flag", default=(List, []), description="Sample exclusion flag"
236
+ )
237
+ SKIP_FILE_CHECKS = Argument(
238
+ name="skip_file_checks",
239
+ alias="-f",
240
+ default=(bool, False),
241
+ description="Do not perform input file checks",
242
+ )
243
+ PACKAGE = Argument(
244
+ name="package",
245
+ alias="-p",
246
+ default=(str, None),
247
+ description="Name of computing resource package to use",
248
+ )
249
+ COMPUTE = Argument(
250
+ name="compute",
251
+ default=(List, []),
252
+ description="List of key-value pairs (k1=v1)",
253
+ )
254
+ DIVVY = Argument(
255
+ name="divvy",
256
+ default=(str, os.getenv("DIVCFG", None)),
257
+ description=(
258
+ "Path to divvy configuration file. Default=$DIVCFG env "
259
+ "variable. Currently: {}".format(os.getenv("DIVCFG") or "not set")
260
+ ),
261
+ )
262
+ # Arguments for logger compatible with logmuse
263
+ SILENT = Argument(
264
+ name="silent", default=(bool, False), description="Whether to silence logging"
265
+ )
266
+ VERBOSITY = Argument(
267
+ name="verbosity",
268
+ default=(int, None),
269
+ description="Alternate mode of expression for logging level that better "
270
+ "accords with intuition about how to convey this.",
271
+ )
272
+ LOGDEV = Argument(
273
+ name="logdev",
274
+ default=(bool, False),
275
+ description="Whether to log in development mode; possibly among other "
276
+ "behavioral changes to logs handling, use a more information-rich "
277
+ "message format template.",
278
+ )
279
+ PIPESTAT = Argument(
280
+ name="pipestat",
281
+ default=(str, None),
282
+ description="Path to pipestat files.",
283
+ )
284
+ PORTABLE = Argument(
285
+ name="portable",
286
+ default=(bool, False),
287
+ description="Makes html report portable.",
288
+ )
289
+ PROJECT_LEVEL = Argument(
290
+ name="project",
291
+ default=(bool, False),
292
+ description="Is this command executed for project-level?",
293
+ )
@@ -0,0 +1,335 @@
1
+ """
2
+ `pydantic` models for `looper` commands and a wrapper class.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import List, Optional, Type, Union
7
+
8
+ import pydantic.v1 as pydantic
9
+
10
+ from ..const import MESSAGE_BY_SUBCOMMAND
11
+ from .arguments import Argument, ArgumentEnum
12
+ from pydantic_argparse import ArgumentParser
13
+
14
+
15
+ @dataclass
16
+ class Command:
17
+ """
18
+ Representation of a command
19
+
20
+ :param str name: command name
21
+ :param str description: command description
22
+ :param list[Argument] arguments: list of arguments supported by this command
23
+ """
24
+
25
+ name: str
26
+ description: str
27
+ arguments: List[Argument]
28
+
29
+ def create_model(self) -> Type[pydantic.BaseModel]:
30
+ """
31
+ Creates a `pydantic` model for this command
32
+ """
33
+ arguments = dict()
34
+ for arg in self.arguments:
35
+ # These gymnastics are necessary because of
36
+ # https://github.com/pydantic/pydantic/issues/2248#issuecomment-757448447
37
+ arg_type, arg_default_value = arg.default
38
+ arguments[arg.name] = (
39
+ arg_type,
40
+ pydantic.Field(arg_default_value, description=arg.description),
41
+ )
42
+ return pydantic.create_model(self.name, **arguments)
43
+
44
+
45
+ SHARED_ARGUMENTS = [
46
+ ArgumentEnum.SETTINGS.value,
47
+ ArgumentEnum.EXC_FLAG.value,
48
+ ArgumentEnum.SEL_FLAG.value,
49
+ ArgumentEnum.SEL_ATTR.value,
50
+ ArgumentEnum.SEL_INCL.value,
51
+ ArgumentEnum.SEL_EXCL.value,
52
+ ArgumentEnum.LIMIT.value,
53
+ ArgumentEnum.SKIP.value,
54
+ ArgumentEnum.PEP_CONFIG.value,
55
+ ArgumentEnum.OUTPUT_DIR.value,
56
+ ArgumentEnum.CONFIG.value,
57
+ ArgumentEnum.SAMPLE_PIPELINE_INTERFACES.value,
58
+ ArgumentEnum.PROJECT_PIPELINE_INTERFACES.value,
59
+ ArgumentEnum.PIPESTAT.value,
60
+ ArgumentEnum.SETTINGS.value,
61
+ ArgumentEnum.AMEND.value,
62
+ ArgumentEnum.PROJECT_LEVEL.value,
63
+ ]
64
+
65
+ RunParser = Command(
66
+ "run",
67
+ MESSAGE_BY_SUBCOMMAND["run"],
68
+ [
69
+ ArgumentEnum.IGNORE_FLAGS.value,
70
+ ArgumentEnum.TIME_DELAY.value,
71
+ ArgumentEnum.DRY_RUN.value,
72
+ ArgumentEnum.COMMAND_EXTRA.value,
73
+ ArgumentEnum.COMMAND_EXTRA_OVERRIDE.value,
74
+ ArgumentEnum.LUMP.value,
75
+ ArgumentEnum.LUMPN.value,
76
+ ArgumentEnum.LUMPJ.value,
77
+ ArgumentEnum.DIVVY.value,
78
+ ArgumentEnum.SKIP_FILE_CHECKS.value,
79
+ ArgumentEnum.COMPUTE.value,
80
+ ArgumentEnum.PACKAGE.value,
81
+ ],
82
+ )
83
+
84
+ # RERUN
85
+ RerunParser = Command(
86
+ "rerun",
87
+ MESSAGE_BY_SUBCOMMAND["rerun"],
88
+ [
89
+ ArgumentEnum.IGNORE_FLAGS.value,
90
+ ArgumentEnum.TIME_DELAY.value,
91
+ ArgumentEnum.DRY_RUN.value,
92
+ ArgumentEnum.COMMAND_EXTRA.value,
93
+ ArgumentEnum.COMMAND_EXTRA_OVERRIDE.value,
94
+ ArgumentEnum.LUMP.value,
95
+ ArgumentEnum.LUMPN.value,
96
+ ArgumentEnum.LUMPJ.value,
97
+ ArgumentEnum.DIVVY.value,
98
+ ArgumentEnum.SKIP_FILE_CHECKS.value,
99
+ ArgumentEnum.COMPUTE.value,
100
+ ArgumentEnum.PACKAGE.value,
101
+ ],
102
+ )
103
+
104
+ # RUNP
105
+ RunProjectParser = Command(
106
+ "runp",
107
+ MESSAGE_BY_SUBCOMMAND["runp"],
108
+ [
109
+ ArgumentEnum.IGNORE_FLAGS.value,
110
+ ArgumentEnum.TIME_DELAY.value,
111
+ ArgumentEnum.DRY_RUN.value,
112
+ ArgumentEnum.COMMAND_EXTRA.value,
113
+ ArgumentEnum.COMMAND_EXTRA_OVERRIDE.value,
114
+ ArgumentEnum.LUMP.value,
115
+ ArgumentEnum.LUMPN.value,
116
+ ArgumentEnum.DIVVY.value,
117
+ ArgumentEnum.SKIP_FILE_CHECKS.value,
118
+ ArgumentEnum.COMPUTE.value,
119
+ ArgumentEnum.PACKAGE.value,
120
+ ],
121
+ )
122
+
123
+ # TABLE
124
+ TableParser = Command(
125
+ "table",
126
+ MESSAGE_BY_SUBCOMMAND["table"],
127
+ [
128
+ ArgumentEnum.REPORT_OUTPUT_DIR.value,
129
+ ],
130
+ )
131
+
132
+
133
+ # REPORT
134
+ ReportParser = Command(
135
+ "report",
136
+ MESSAGE_BY_SUBCOMMAND["report"],
137
+ [
138
+ ArgumentEnum.PORTABLE.value,
139
+ ArgumentEnum.REPORT_OUTPUT_DIR.value,
140
+ ],
141
+ )
142
+
143
+ # DESTROY
144
+ DestroyParser = Command(
145
+ "destroy",
146
+ MESSAGE_BY_SUBCOMMAND["destroy"],
147
+ [
148
+ ArgumentEnum.DRY_RUN.value,
149
+ ArgumentEnum.FORCE_YES.value,
150
+ ],
151
+ )
152
+
153
+ # CHECK
154
+ CheckParser = Command(
155
+ "check",
156
+ MESSAGE_BY_SUBCOMMAND["check"],
157
+ [
158
+ ArgumentEnum.DESCRIBE_CODES.value,
159
+ ArgumentEnum.ITEMIZED.value,
160
+ ArgumentEnum.FLAGS.value,
161
+ ],
162
+ )
163
+
164
+ # CLEAN
165
+ CleanParser = Command(
166
+ "clean",
167
+ MESSAGE_BY_SUBCOMMAND["clean"],
168
+ [
169
+ ArgumentEnum.DRY_RUN.value,
170
+ ArgumentEnum.FORCE_YES.value,
171
+ ],
172
+ )
173
+
174
+ # INSPECT
175
+ InspectParser = Command(
176
+ "inspect",
177
+ MESSAGE_BY_SUBCOMMAND["inspect"],
178
+ [],
179
+ )
180
+
181
+
182
+ # INIT
183
+ InitParser = Command(
184
+ "init",
185
+ MESSAGE_BY_SUBCOMMAND["init"],
186
+ [
187
+ # Original command has force flag which is technically a different flag, but we should just use FORCE_YES
188
+ ArgumentEnum.FORCE_YES.value,
189
+ ArgumentEnum.OUTPUT_DIR.value,
190
+ ArgumentEnum.PEP_CONFIG.value,
191
+ ArgumentEnum.SAMPLE_PIPELINE_INTERFACES.value,
192
+ ArgumentEnum.PROJECT_PIPELINE_INTERFACES.value,
193
+ ArgumentEnum.GENERIC.value,
194
+ ],
195
+ )
196
+
197
+
198
+ # INIT-PIFACE
199
+ InitPifaceParser = Command(
200
+ "init_piface",
201
+ MESSAGE_BY_SUBCOMMAND["init-piface"],
202
+ [],
203
+ )
204
+
205
+
206
+ # LINK
207
+ LinkParser = Command(
208
+ "link",
209
+ MESSAGE_BY_SUBCOMMAND["link"],
210
+ [],
211
+ )
212
+
213
+
214
+ # Add shared arguments for all commands that use them
215
+ for arg in SHARED_ARGUMENTS:
216
+ RunParser.arguments.append(arg)
217
+ RerunParser.arguments.append(arg)
218
+ RunProjectParser.arguments.append(arg)
219
+ ReportParser.arguments.append(arg)
220
+ DestroyParser.arguments.append(arg)
221
+ CheckParser.arguments.append(arg)
222
+ CleanParser.arguments.append(arg)
223
+ TableParser.arguments.append(arg)
224
+ LinkParser.arguments.append(arg)
225
+ InspectParser.arguments.append(arg)
226
+
227
+ # Create all Models
228
+ RunParserModel = RunParser.create_model()
229
+ RerunParserModel = RerunParser.create_model()
230
+ RunProjectParserModel = RunProjectParser.create_model()
231
+ ReportParserModel = ReportParser.create_model()
232
+ DestroyParserModel = DestroyParser.create_model()
233
+ CheckParserModel = CheckParser.create_model()
234
+ CleanParserModel = CleanParser.create_model()
235
+ TableParserModel = TableParser.create_model()
236
+ LinkParserModel = LinkParser.create_model()
237
+ InspectParserModel = InspectParser.create_model()
238
+ InitParserModel = InitParser.create_model()
239
+ InitPifaceParserModel = InitPifaceParser.create_model()
240
+
241
+
242
+ def add_short_arguments(
243
+ parser: ArgumentParser, argument_enums: Type[ArgumentEnum]
244
+ ) -> ArgumentParser:
245
+ """
246
+ This function takes a parser object created under pydantic argparse and adds the short arguments AFTER the initial creation.
247
+ This is a workaround as pydantic-argparse does not currently support this during initial parser creation.
248
+
249
+ :param ArgumentParser parser: parser before adding short arguments
250
+ :param Type[ArgumentEnum] argument_enums: enumeration of arguments that contain names and aliases
251
+ :return ArgumentParser parser: parser after short arguments have been added
252
+ """
253
+
254
+ for cmd in parser._subcommands.choices.keys():
255
+
256
+ for argument_enum in list(argument_enums):
257
+ # First check there is an alias for the argument otherwise skip
258
+ if argument_enum.value.alias:
259
+ short_key = argument_enum.value.alias
260
+ long_key = "--" + argument_enum.value.name.replace(
261
+ "_", "-"
262
+ ) # We must do this because the ArgumentEnum names are transformed during parser creation
263
+ if long_key in parser._subcommands.choices[cmd]._option_string_actions:
264
+ argument = parser._subcommands.choices[cmd]._option_string_actions[
265
+ long_key
266
+ ]
267
+ argument.option_strings = (short_key, long_key)
268
+ parser._subcommands.choices[cmd]._option_string_actions[
269
+ short_key
270
+ ] = argument
271
+
272
+ return parser
273
+
274
+
275
+ SUPPORTED_COMMANDS = [
276
+ RunParser,
277
+ RerunParser,
278
+ RunProjectParser,
279
+ TableParser,
280
+ ReportParser,
281
+ DestroyParser,
282
+ CheckParser,
283
+ CleanParser,
284
+ InitParser,
285
+ InitPifaceParser,
286
+ LinkParser,
287
+ InspectParser,
288
+ ]
289
+
290
+
291
+ class TopLevelParser(pydantic.BaseModel):
292
+ """
293
+ Top level parser that takes
294
+ - commands (run, runp, check...)
295
+ - arguments that are required no matter the subcommand
296
+ """
297
+
298
+ # commands
299
+ run: Optional[RunParserModel] = pydantic.Field(description=RunParser.description)
300
+ rerun: Optional[RerunParserModel] = pydantic.Field(
301
+ description=RerunParser.description
302
+ )
303
+ runp: Optional[RunProjectParserModel] = pydantic.Field(
304
+ description=RunProjectParser.description
305
+ )
306
+ table: Optional[TableParserModel] = pydantic.Field(
307
+ description=TableParser.description
308
+ )
309
+ report: Optional[ReportParserModel] = pydantic.Field(
310
+ description=ReportParser.description
311
+ )
312
+ destroy: Optional[DestroyParserModel] = pydantic.Field(
313
+ description=DestroyParser.description
314
+ )
315
+ check: Optional[CheckParserModel] = pydantic.Field(
316
+ description=CheckParser.description
317
+ )
318
+ clean: Optional[CleanParserModel] = pydantic.Field(
319
+ description=CleanParser.description
320
+ )
321
+ init: Optional[InitParserModel] = pydantic.Field(description=InitParser.description)
322
+ init_piface: Optional[InitPifaceParserModel] = pydantic.Field(
323
+ description=InitPifaceParser.description
324
+ )
325
+ link: Optional[LinkParserModel] = pydantic.Field(description=LinkParser.description)
326
+
327
+ inspect: Optional[InspectParserModel] = pydantic.Field(
328
+ description=InspectParser.description
329
+ )
330
+
331
+ # Additional arguments for logging, added to ALL commands
332
+ # These must be used before the command
333
+ silent: Optional[bool] = ArgumentEnum.SILENT.value.with_reduced_default()
334
+ verbosity: Optional[int] = ArgumentEnum.VERBOSITY.value.with_reduced_default()
335
+ logdev: Optional[bool] = ArgumentEnum.LOGDEV.value.with_reduced_default()