looper 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
looper/__main__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- from .cli_looper import main
3
+ from .cli_pydantic import main
4
4
  from .cli_divvy import main as divvy_main
5
5
 
6
6
  if __name__ == "__main__":
looper/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.7.1"
1
+ __version__ = "1.8.0"
looper/cli_pydantic.py ADDED
@@ -0,0 +1,385 @@
1
+ """
2
+ CLI script using `pydantic-argparse` for parsing of arguments
3
+
4
+ Arguments / commands are defined in `command_models/` and are given, eventually, as
5
+ `pydantic` models, allowing for type-checking and validation of arguments.
6
+
7
+ Note: this is only a test script so far, and coexists next to the current CLI
8
+ (`cli_looper.py`), which uses `argparse` directly. The goal is to eventually
9
+ replace the current CLI with a CLI based on above-mentioned `pydantic` models,
10
+ but whether this will happen with `pydantic-argparse` or another, possibly self-
11
+ written library is not yet clear.
12
+ It is well possible that this script will be removed again.
13
+ """
14
+
15
+ # Note: The following import is used for forward annotations (Python 3.8)
16
+ # to prevent potential 'TypeError' related to the use of the '|' operator
17
+ # with types.
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+
23
+ import logmuse
24
+ import pydantic2_argparse
25
+ import yaml
26
+ from eido import inspect_project
27
+ from pephubclient import PEPHubClient
28
+ from pydantic2_argparse.argparse.parser import ArgumentParser
29
+
30
+ from divvy import select_divvy_config
31
+
32
+ from .const import PipelineLevel
33
+ from . import __version__
34
+
35
+ from .command_models.arguments import ArgumentEnum
36
+
37
+ from .command_models.commands import (
38
+ SUPPORTED_COMMANDS,
39
+ TopLevelParser,
40
+ add_short_arguments,
41
+ )
42
+ from .const import *
43
+ from .divvy import DEFAULT_COMPUTE_RESOURCES_NAME, select_divvy_config
44
+ from .exceptions import *
45
+ from .looper import *
46
+ from .parser_types import *
47
+ from .project import Project, ProjectContext
48
+ from .utils import (
49
+ dotfile_path,
50
+ enrich_args_via_cfg,
51
+ is_pephub_registry_path,
52
+ read_looper_config_file,
53
+ read_looper_dotfile,
54
+ initiate_looper_config,
55
+ init_generic_pipeline,
56
+ read_yaml_file,
57
+ inspect_looper_config_file,
58
+ is_PEP_file_type,
59
+ )
60
+
61
+ from typing import List, Tuple
62
+
63
+
64
+ def opt_attr_pair(name: str) -> Tuple[str, str]:
65
+ """Takes argument as attribute and returns as tuple of top-level or subcommand used."""
66
+ return f"--{name}", name.replace("-", "_")
67
+
68
+
69
+ def validate_post_parse(args: argparse.Namespace) -> List[str]:
70
+ """Checks if user is attempting to use mutually exclusive options."""
71
+ problems = []
72
+ used_exclusives = [
73
+ opt
74
+ for opt, attr in map(
75
+ opt_attr_pair,
76
+ [
77
+ "skip",
78
+ "limit",
79
+ SAMPLE_EXCLUSION_OPTNAME,
80
+ SAMPLE_INCLUSION_OPTNAME,
81
+ ],
82
+ )
83
+ # Depending on the subcommand used, the above options might either be in
84
+ # the top-level namespace or in the subcommand namespace (the latter due
85
+ # to a `modify_args_namespace()`)
86
+ if getattr(
87
+ args, attr, None
88
+ ) # or (getattr(args.run, attr, None) if hasattr(args, "run") else False)
89
+ ]
90
+ if len(used_exclusives) > 1:
91
+ problems.append(
92
+ f"Used multiple mutually exclusive options: {', '.join(used_exclusives)}"
93
+ )
94
+ return problems
95
+
96
+
97
+ # TODO rename to run_looper_via_cli for running lloper as a python library:
98
+ # https://github.com/pepkit/looper/pull/472#discussion_r1521970763
99
+ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
100
+ # here comes adapted `cli_looper.py` code
101
+ global _LOGGER
102
+
103
+ _LOGGER = logmuse.logger_via_cli(args, make_root=True)
104
+
105
+ # Find out which subcommand was used
106
+ supported_command_names = [cmd.name for cmd in SUPPORTED_COMMANDS]
107
+ subcommand_valued_args = [
108
+ (arg, value)
109
+ for arg, value in vars(args).items()
110
+ if arg and arg in supported_command_names and value is not None
111
+ ]
112
+ # Only one subcommand argument will be not `None`, else we found a bug in `pydantic-argparse`
113
+ [(subcommand_name, subcommand_args)] = subcommand_valued_args
114
+
115
+ cli_use_errors = validate_post_parse(subcommand_args)
116
+ if cli_use_errors:
117
+ parser.print_help(sys.stderr)
118
+ parser.error(
119
+ f"{len(cli_use_errors)} CLI use problem(s): {', '.join(cli_use_errors)}"
120
+ )
121
+
122
+ if subcommand_name is None:
123
+ parser.print_help(sys.stderr)
124
+ sys.exit(1)
125
+
126
+ if subcommand_name == "init":
127
+ return int(
128
+ not initiate_looper_config(
129
+ dotfile_path(),
130
+ subcommand_args.pep_config,
131
+ subcommand_args.output_dir,
132
+ subcommand_args.sample_pipeline_interfaces,
133
+ subcommand_args.project_pipeline_interfaces,
134
+ subcommand_args.force_yes,
135
+ )
136
+ )
137
+
138
+ if subcommand_name == "init_piface":
139
+ sys.exit(int(not init_generic_pipeline()))
140
+
141
+ _LOGGER.info("Looper version: {}\nCommand: {}".format(__version__, subcommand_name))
142
+
143
+ if subcommand_args.config_file is None:
144
+ looper_cfg_path = os.path.relpath(dotfile_path(), start=os.curdir)
145
+ try:
146
+ if subcommand_args.looper_config:
147
+ looper_config_dict = read_looper_config_file(
148
+ subcommand_args.looper_config
149
+ )
150
+ else:
151
+ looper_config_dict = read_looper_dotfile()
152
+ _LOGGER.info(f"Using looper config ({looper_cfg_path}).")
153
+
154
+ for looper_config_key, looper_config_item in looper_config_dict.items():
155
+ setattr(subcommand_args, looper_config_key, looper_config_item)
156
+
157
+ except OSError:
158
+ parser.print_help(sys.stderr)
159
+ _LOGGER.warning(
160
+ f"Looper config file does not exist. Use looper init to create one at {looper_cfg_path}."
161
+ )
162
+ sys.exit(1)
163
+ else:
164
+ _LOGGER.warning(
165
+ "This PEP configures looper through the project config. This approach is deprecated and will "
166
+ "be removed in future versions. Please use a looper config file. For more information see "
167
+ "looper.databio.org/en/latest/looper-config"
168
+ )
169
+
170
+ subcommand_args = enrich_args_via_cfg(
171
+ subcommand_name, subcommand_args, parser, test_args=test_args
172
+ )
173
+
174
+ # If project pipeline interface defined in the cli, change name to: "pipeline_interface"
175
+ if vars(subcommand_args)[PROJECT_PL_ARG]:
176
+ subcommand_args.pipeline_interfaces = vars(subcommand_args)[PROJECT_PL_ARG]
177
+
178
+ divcfg = (
179
+ select_divvy_config(filepath=subcommand_args.divvy)
180
+ if hasattr(subcommand_args, "divvy")
181
+ else None
182
+ )
183
+ # Ignore flags if user is selecting or excluding on flags:
184
+ if subcommand_args.sel_flag or subcommand_args.exc_flag:
185
+ subcommand_args.ignore_flags = True
186
+
187
+ # Initialize project
188
+ if is_PEP_file_type(subcommand_args.config_file) and os.path.exists(
189
+ subcommand_args.config_file
190
+ ):
191
+ try:
192
+ p = Project(
193
+ cfg=subcommand_args.config_file,
194
+ amendments=subcommand_args.amend,
195
+ divcfg_path=divcfg,
196
+ runp=subcommand_name == "runp",
197
+ **{
198
+ attr: getattr(subcommand_args, attr)
199
+ for attr in CLI_PROJ_ATTRS
200
+ if attr in subcommand_args
201
+ },
202
+ )
203
+ except yaml.parser.ParserError as e:
204
+ _LOGGER.error(f"Project config parse failed -- {e}")
205
+ sys.exit(1)
206
+ elif is_pephub_registry_path(subcommand_args.config_file):
207
+ if vars(subcommand_args)[SAMPLE_PL_ARG]:
208
+ p = Project(
209
+ amendments=subcommand_args.amend,
210
+ divcfg_path=divcfg,
211
+ runp=subcommand_name == "runp",
212
+ project_dict=PEPHubClient()._load_raw_pep(
213
+ registry_path=subcommand_args.config_file
214
+ ),
215
+ **{
216
+ attr: getattr(subcommand_args, attr)
217
+ for attr in CLI_PROJ_ATTRS
218
+ if attr in subcommand_args
219
+ },
220
+ )
221
+ else:
222
+ raise MisconfigurationException(
223
+ f"`sample_pipeline_interface` is missing. Provide it in the parameters."
224
+ )
225
+ else:
226
+ raise MisconfigurationException(
227
+ f"Cannot load PEP. Check file path or registry path to pep."
228
+ )
229
+
230
+ selected_compute_pkg = p.selected_compute_package or DEFAULT_COMPUTE_RESOURCES_NAME
231
+ if p.dcc is not None and not p.dcc.activate_package(selected_compute_pkg):
232
+ _LOGGER.info(
233
+ "Failed to activate '{}' computing package. "
234
+ "Using the default one".format(selected_compute_pkg)
235
+ )
236
+
237
+ with ProjectContext(
238
+ prj=p,
239
+ selector_attribute=subcommand_args.sel_attr,
240
+ selector_include=subcommand_args.sel_incl,
241
+ selector_exclude=subcommand_args.sel_excl,
242
+ selector_flag=subcommand_args.sel_flag,
243
+ exclusion_flag=subcommand_args.exc_flag,
244
+ ) as prj:
245
+
246
+ # Check at the beginning if user wants to use pipestat and pipestat is configurable
247
+ is_pipestat_configured = (
248
+ prj._check_if_pipestat_configured(pipeline_type=PipelineLevel.PROJECT.value)
249
+ if getattr(args, "project", None)
250
+ else prj._check_if_pipestat_configured()
251
+ )
252
+
253
+ if subcommand_name in ["run", "rerun"]:
254
+ rerun = subcommand_name == "rerun"
255
+ run = Runner(prj)
256
+ try:
257
+ # compute_kwargs = _proc_resources_spec(args)
258
+ compute_kwargs = _proc_resources_spec(subcommand_args)
259
+
260
+ # TODO Shouldn't top level args and subcommand args be accessible on the same object?
261
+ return run(
262
+ subcommand_args, top_level_args=args, rerun=rerun, **compute_kwargs
263
+ )
264
+ except SampleFailedException:
265
+ sys.exit(1)
266
+ except IOError:
267
+ _LOGGER.error(
268
+ "{} pipeline_interfaces: '{}'".format(
269
+ prj.__class__.__name__, prj.pipeline_interface_sources
270
+ )
271
+ )
272
+ raise
273
+
274
+ if subcommand_name == "runp":
275
+ compute_kwargs = _proc_resources_spec(subcommand_args)
276
+ collate = Collator(prj)
277
+ collate(subcommand_args, **compute_kwargs)
278
+ return collate.debug
279
+
280
+ if subcommand_name == "destroy":
281
+ return Destroyer(prj)(subcommand_args)
282
+
283
+ if subcommand_name == "table":
284
+ if is_pipestat_configured:
285
+ return Tabulator(prj)(subcommand_args)
286
+ else:
287
+ raise PipestatConfigurationException("table")
288
+
289
+ if subcommand_name == "report":
290
+ if is_pipestat_configured:
291
+ return Reporter(prj)(subcommand_args)
292
+ else:
293
+ raise PipestatConfigurationException("report")
294
+
295
+ if subcommand_name == "link":
296
+ if is_pipestat_configured:
297
+ Linker(prj)(subcommand_args)
298
+ else:
299
+ raise PipestatConfigurationException("link")
300
+
301
+ if subcommand_name == "check":
302
+ if is_pipestat_configured:
303
+ return Checker(prj)(subcommand_args)
304
+ else:
305
+ raise PipestatConfigurationException("check")
306
+
307
+ if subcommand_name == "clean":
308
+ return Cleaner(prj)(subcommand_args)
309
+
310
+ if subcommand_name == "inspect":
311
+ # Inspect PEP from Eido
312
+ sample_names = []
313
+ for sample in p.samples:
314
+ sample_names.append(sample["sample_name"])
315
+ inspect_project(p, sample_names)
316
+ # Inspect looper config file
317
+ if looper_config_dict:
318
+ inspect_looper_config_file(looper_config_dict)
319
+ else:
320
+ _LOGGER.warning("No looper configuration was supplied.")
321
+
322
+
323
+ def main(test_args=None) -> None:
324
+ parser = pydantic2_argparse.ArgumentParser(
325
+ model=TopLevelParser,
326
+ prog="looper",
327
+ description="Looper Pydantic Argument Parser",
328
+ add_help=True,
329
+ )
330
+
331
+ parser = add_short_arguments(parser, ArgumentEnum)
332
+
333
+ if test_args:
334
+ args = parser.parse_typed_args(args=test_args)
335
+ else:
336
+ args = parser.parse_typed_args()
337
+
338
+ return run_looper(args, parser, test_args=test_args)
339
+
340
+
341
+ def _proc_resources_spec(args):
342
+ """
343
+ Process CLI-sources compute setting specification. There are two sources
344
+ of compute settings in the CLI alone:
345
+ * YAML file (--settings argument)
346
+ * itemized compute settings (--compute argument)
347
+
348
+ The itemized compute specification is given priority
349
+
350
+ :param argparse.Namespace: arguments namespace
351
+ :return Mapping[str, str]: binding between resource setting name and value
352
+ :raise ValueError: if interpretation of the given specification as encoding
353
+ of key-value pairs fails
354
+ """
355
+ spec = getattr(args, "compute", None)
356
+ settings = args.settings
357
+ try:
358
+ settings_data = read_yaml_file(settings) or {}
359
+ except yaml.YAMLError:
360
+ _LOGGER.warning(
361
+ "Settings file ({}) does not follow YAML format,"
362
+ " disregarding".format(settings)
363
+ )
364
+ settings_data = {}
365
+ if not spec:
366
+ return settings_data
367
+ pairs = [(kv, kv.split("=")) for kv in spec]
368
+ bads = []
369
+ for orig, pair in pairs:
370
+ try:
371
+ k, v = pair
372
+ except ValueError:
373
+ bads.append(orig)
374
+ else:
375
+ settings_data[k] = v
376
+ if bads:
377
+ raise ValueError(
378
+ "Could not correctly parse itemized compute specification. "
379
+ "Correct format: " + EXAMPLE_COMPUTE_SPEC_FMT
380
+ )
381
+ return settings_data
382
+
383
+
384
+ if __name__ == "__main__":
385
+ main()
@@ -0,0 +1,85 @@
1
+ # Developer documentation
2
+
3
+ ## Adding new command models
4
+
5
+ To add a new model (command) to the project, follow these steps:
6
+
7
+ 1. Add new arguments in `looper/command_models/arguments.py` if necessary.
8
+
9
+ - Add a new entry for the `ArgumentEnum` class.
10
+ - For example:
11
+
12
+ ```python
13
+ # arguments.py
14
+
15
+ class ArgumentEnum(enum.Enum):
16
+ ...
17
+
18
+ NEW_ARGUMENT = Argument(
19
+ name="new_argument",
20
+ default=(new_argument_type, "default_value"),
21
+ description="Description of the new argument",
22
+ )
23
+
24
+ ```
25
+
26
+ 2. Create a new command in the existing command creation logic in `looper/command_models/commands.py`.
27
+
28
+ - Create a new `Command` instance.
29
+ - Create a `pydantic` model for this new command.
30
+ - Add the new `Command` instance to `SUPPORTED_COMMANDS`.
31
+ - For example:
32
+
33
+ ```python
34
+ NewCommandParser = Command(
35
+ "new_command",
36
+ MESSAGE_BY_SUBCOMMAND["new_command"],
37
+ [
38
+ ...
39
+ ArgumentEnum.NEW_ARGUMENT.value,
40
+ # Add more arguments as needed for the new command
41
+ ],
42
+ )
43
+ NewCommandParserModel = NewCommandParser.create_model()
44
+
45
+ SUPPORTED_COMMANDS = [..., NewCommandParser]
46
+ ```
47
+
48
+ 3. Update the new argument(s) and command in `TopLevelParser` from `looper/command_models/commands.py`.
49
+
50
+ - Add a new field for the new command.
51
+ - Add a new field for the new argument(s).
52
+ - For example:
53
+
54
+ ```python
55
+ class TopLevelParser(pydantic.BaseModel):
56
+
57
+ # commands
58
+ ...
59
+ new_command: Optional[NewCommandParserModel] = pydantic.Field(description=NewCommandParser.description)
60
+
61
+ # arguments
62
+ ...
63
+ new_argument: Optional[new_argument_type] = ArgumentEnum.NEW_ARGUMENT.value.with_reduced_default()
64
+ ```
65
+
66
+ ## Special treatment for the `run` command
67
+
68
+ The `run` command in our project requires special treatment to accommodate hierarchical namespaces
69
+ and properly handle its unique characteristics. Several functions have been adapted to ensure the
70
+ correct behavior of the run command, and similar adaptations may be necessary for other commands.
71
+
72
+ For developers looking to understand the details of the special treatment given to the `run`
73
+ command and its associated changes, we recommend to inspect the following functions / part of the
74
+ code:
75
+ - `looper/cli_looper.py`:
76
+ - `make_hierarchical_if_needed()`
77
+ - assignment of the `divcfg` variable
78
+ - assignment of the `project_args` variable
79
+ - `_proc_resources_spec()`
80
+ - `validate_post_parse()`
81
+ - `looper/utils.py`:
82
+ - `enrich_args_via_cfg()`
83
+
84
+ If you are adding new commands to the project / migrate existing commands to a `pydantic` model-based definition, adapt these parts of the codes with equivalent behavior for your new command.
85
+ Likewise, adapt argument accessions in the corresponding executor in `looper/looper.py` to take into account the hierarchical organization of the command's arguments.
@@ -0,0 +1,4 @@
1
+ # `pydantic`-based definitions of `looper` commands and their arguments
2
+
3
+ With the goal of writing an HTTP API that is in sync with the `looper` CLI, this module defines `looper` commands as `pydantic` models and arguments as fields in there.
4
+ These can then be used by the [`pydantic-argparse`](https://pydantic-argparse.supimdos.com/) library to create a type-validated CLI (see `../cli_pydantic.py`), and by the future HTTP API for validating `POST`ed JSON data. Eventually, the `pydantic-argparse`-based CLI will replace the existing `argparse`-based CLI defined in `../cli_looper.py`.
@@ -0,0 +1,6 @@
1
+ """
2
+ This package holds `pydantic` models that describe commands and their arguments.
3
+
4
+ These can be used either by an HTTP API or with the `pydantic-argparse`
5
+ library to build a CLI.
6
+ """