looper 1.9.1__py3-none-any.whl → 2.0.0a1__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/_version.py +2 -2
- looper/cli_pydantic.py +53 -43
- looper/command_models/arguments.py +3 -8
- looper/command_models/commands.py +1 -2
- looper/conductor.py +8 -1
- looper/pipeline_interface.py +0 -9
- looper/project.py +9 -7
- looper/schemas/pipeline_interface_schema_generic.yaml +14 -6
- looper/utils.py +255 -65
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/METADATA +2 -2
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/RECORD +15 -15
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/WHEEL +1 -1
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/LICENSE.txt +0 -0
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/entry_points.txt +0 -0
- {looper-1.9.1.dist-info → looper-2.0.0a1.dist-info}/top_level.txt +0 -0
looper/_version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__ = "
|
2
|
-
# You must change the version in parser =
|
1
|
+
__version__ = "2.0.0a1"
|
2
|
+
# You must change the version in parser = pydantic_argparse.ArgumentParser in cli_pydantic.py!!!
|
looper/cli_pydantic.py
CHANGED
@@ -54,9 +54,11 @@ from .utils import (
|
|
54
54
|
read_yaml_file,
|
55
55
|
inspect_looper_config_file,
|
56
56
|
is_PEP_file_type,
|
57
|
+
looper_config_tutorial,
|
57
58
|
)
|
58
59
|
|
59
60
|
from typing import List, Tuple
|
61
|
+
from rich.console import Console
|
60
62
|
|
61
63
|
|
62
64
|
def opt_attr_pair(name: str) -> Tuple[str, str]:
|
@@ -122,52 +124,60 @@ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
|
|
122
124
|
sys.exit(1)
|
123
125
|
|
124
126
|
if subcommand_name == "init":
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
subcommand_args.project_pipeline_interfaces,
|
132
|
-
subcommand_args.force_yes,
|
133
|
-
)
|
127
|
+
|
128
|
+
console = Console()
|
129
|
+
console.clear()
|
130
|
+
console.rule(f"\n[magenta]Looper initialization[/magenta]")
|
131
|
+
console.print(
|
132
|
+
"[bold]Would you like to follow a guided tutorial?[/bold] [green]Y[/green] / [red]n[/red]..."
|
134
133
|
)
|
135
134
|
|
135
|
+
selection = None
|
136
|
+
while selection not in ["y", "n"]:
|
137
|
+
selection = console.input("\nSelection: ").lower().strip()
|
138
|
+
|
139
|
+
if selection == "n":
|
140
|
+
console.clear()
|
141
|
+
return int(
|
142
|
+
not initiate_looper_config(
|
143
|
+
dotfile_path(),
|
144
|
+
subcommand_args.pep_config,
|
145
|
+
subcommand_args.output_dir,
|
146
|
+
subcommand_args.sample_pipeline_interfaces,
|
147
|
+
subcommand_args.project_pipeline_interfaces,
|
148
|
+
subcommand_args.force_yes,
|
149
|
+
)
|
150
|
+
)
|
151
|
+
else:
|
152
|
+
console.clear()
|
153
|
+
return int(looper_config_tutorial())
|
154
|
+
|
136
155
|
if subcommand_name == "init_piface":
|
137
156
|
sys.exit(int(not init_generic_pipeline()))
|
138
157
|
|
139
158
|
_LOGGER.info("Looper version: {}\nCommand: {}".format(__version__, subcommand_name))
|
140
159
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
160
|
+
looper_cfg_path = os.path.relpath(dotfile_path(), start=os.curdir)
|
161
|
+
try:
|
162
|
+
if subcommand_args.config:
|
163
|
+
looper_config_dict = read_looper_config_file(subcommand_args.config)
|
164
|
+
else:
|
165
|
+
looper_config_dict = read_looper_dotfile()
|
166
|
+
_LOGGER.info(f"Using looper config ({looper_cfg_path}).")
|
167
|
+
|
168
|
+
cli_modifiers_dict = None
|
169
|
+
for looper_config_key, looper_config_item in looper_config_dict.items():
|
170
|
+
if looper_config_key == CLI_KEY:
|
171
|
+
cli_modifiers_dict = looper_config_item
|
148
172
|
else:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
for looper_config_key, looper_config_item in looper_config_dict.items():
|
154
|
-
if looper_config_key == CLI_KEY:
|
155
|
-
cli_modifiers_dict = looper_config_item
|
156
|
-
else:
|
157
|
-
setattr(subcommand_args, looper_config_key, looper_config_item)
|
158
|
-
|
159
|
-
except OSError:
|
160
|
-
parser.print_help(sys.stderr)
|
161
|
-
_LOGGER.warning(
|
162
|
-
f"Looper config file does not exist. Use looper init to create one at {looper_cfg_path}."
|
163
|
-
)
|
164
|
-
sys.exit(1)
|
165
|
-
else:
|
173
|
+
setattr(subcommand_args, looper_config_key, looper_config_item)
|
174
|
+
|
175
|
+
except OSError:
|
176
|
+
parser.print_help(sys.stderr)
|
166
177
|
_LOGGER.warning(
|
167
|
-
"
|
168
|
-
"be removed in future versions. Please use a looper config file. For more information see "
|
169
|
-
"looper.databio.org/en/latest/looper-config"
|
178
|
+
f"Looper config file does not exist. Use looper init to create one at {looper_cfg_path}."
|
170
179
|
)
|
180
|
+
sys.exit(1)
|
171
181
|
|
172
182
|
subcommand_args = enrich_args_via_cfg(
|
173
183
|
subcommand_name,
|
@@ -191,12 +201,12 @@ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
|
|
191
201
|
subcommand_args.ignore_flags = True
|
192
202
|
|
193
203
|
# Initialize project
|
194
|
-
if is_PEP_file_type(subcommand_args.
|
195
|
-
subcommand_args.
|
204
|
+
if is_PEP_file_type(subcommand_args.pep_config) and os.path.exists(
|
205
|
+
subcommand_args.pep_config
|
196
206
|
):
|
197
207
|
try:
|
198
208
|
p = Project(
|
199
|
-
cfg=subcommand_args.
|
209
|
+
cfg=subcommand_args.pep_config,
|
200
210
|
amendments=subcommand_args.amend,
|
201
211
|
divcfg_path=divcfg,
|
202
212
|
runp=subcommand_name == "runp",
|
@@ -209,14 +219,14 @@ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
|
|
209
219
|
except yaml.parser.ParserError as e:
|
210
220
|
_LOGGER.error(f"Project config parse failed -- {e}")
|
211
221
|
sys.exit(1)
|
212
|
-
elif is_pephub_registry_path(subcommand_args.
|
222
|
+
elif is_pephub_registry_path(subcommand_args.pep_config):
|
213
223
|
if vars(subcommand_args)[SAMPLE_PL_ARG]:
|
214
224
|
p = Project(
|
215
225
|
amendments=subcommand_args.amend,
|
216
226
|
divcfg_path=divcfg,
|
217
227
|
runp=subcommand_name == "runp",
|
218
228
|
project_dict=PEPHubClient()._load_raw_pep(
|
219
|
-
registry_path=subcommand_args.
|
229
|
+
registry_path=subcommand_args.pep_config
|
220
230
|
),
|
221
231
|
**{
|
222
232
|
attr: getattr(subcommand_args, attr)
|
@@ -252,7 +262,7 @@ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
|
|
252
262
|
# Check at the beginning if user wants to use pipestat and pipestat is configurable
|
253
263
|
is_pipestat_configured = (
|
254
264
|
prj._check_if_pipestat_configured(pipeline_type=PipelineLevel.PROJECT.value)
|
255
|
-
if getattr(subcommand_args, "project", None)
|
265
|
+
if getattr(subcommand_args, "project", None) or subcommand_name == "runp"
|
256
266
|
else prj._check_if_pipestat_configured()
|
257
267
|
)
|
258
268
|
|
@@ -336,7 +346,7 @@ def main(test_args=None) -> None:
|
|
336
346
|
prog="looper",
|
337
347
|
description="Looper: A job submitter for Portable Encapsulated Projects",
|
338
348
|
add_help=True,
|
339
|
-
version="
|
349
|
+
version="2.0.0a1",
|
340
350
|
)
|
341
351
|
|
342
352
|
parser = add_short_arguments(parser, ArgumentEnum)
|
@@ -162,13 +162,9 @@ class ArgumentEnum(enum.Enum):
|
|
162
162
|
default=(int, None),
|
163
163
|
description="Skip samples by numerical index",
|
164
164
|
)
|
165
|
-
|
166
|
-
name="
|
167
|
-
|
168
|
-
description="Project configuration file",
|
169
|
-
)
|
170
|
-
LOOPER_CONFIG = Argument(
|
171
|
-
name="looper_config",
|
165
|
+
CONFIG = Argument(
|
166
|
+
name="config",
|
167
|
+
alias="-c",
|
172
168
|
default=(str, None),
|
173
169
|
description="Looper configuration file (YAML)",
|
174
170
|
)
|
@@ -237,7 +233,6 @@ class ArgumentEnum(enum.Enum):
|
|
237
233
|
)
|
238
234
|
COMPUTE = Argument(
|
239
235
|
name="compute",
|
240
|
-
alias="-c",
|
241
236
|
default=(List, []),
|
242
237
|
description="List of key-value pairs (k1=v1)",
|
243
238
|
)
|
@@ -53,8 +53,7 @@ SHARED_ARGUMENTS = [
|
|
53
53
|
ArgumentEnum.SKIP.value,
|
54
54
|
ArgumentEnum.PEP_CONFIG.value,
|
55
55
|
ArgumentEnum.OUTPUT_DIR.value,
|
56
|
-
ArgumentEnum.
|
57
|
-
ArgumentEnum.LOOPER_CONFIG.value,
|
56
|
+
ArgumentEnum.CONFIG.value,
|
58
57
|
ArgumentEnum.SAMPLE_PIPELINE_INTERFACES.value,
|
59
58
|
ArgumentEnum.PROJECT_PIPELINE_INTERFACES.value,
|
60
59
|
ArgumentEnum.PIPESTAT.value,
|
looper/conductor.py
CHANGED
@@ -198,6 +198,9 @@ class SubmissionConductor(object):
|
|
198
198
|
|
199
199
|
self.collate = collate
|
200
200
|
self.section_key = PROJECT_PL_KEY if self.collate else SAMPLE_PL_KEY
|
201
|
+
self.pipeline_interface_type = (
|
202
|
+
"project_interface" if self.collate else "sample_interface"
|
203
|
+
)
|
201
204
|
self.pl_iface = pipeline_interface
|
202
205
|
self.pl_name = self.pl_iface.pipeline_name
|
203
206
|
self.prj = prj
|
@@ -681,7 +684,11 @@ class SubmissionConductor(object):
|
|
681
684
|
pipeline=self.pl_iface,
|
682
685
|
compute=self.prj.dcc.compute,
|
683
686
|
)
|
684
|
-
|
687
|
+
|
688
|
+
if self.pipeline_interface_type is None:
|
689
|
+
templ = self.pl_iface["command_template"]
|
690
|
+
else:
|
691
|
+
templ = self.pl_iface[self.pipeline_interface_type]["command_template"]
|
685
692
|
if not self.override_extra:
|
686
693
|
extras_template = (
|
687
694
|
EXTRA_PROJECT_CMD_TEMPLATE
|
looper/pipeline_interface.py
CHANGED
@@ -56,15 +56,6 @@ class PipelineInterface(YAMLConfigManager):
|
|
56
56
|
)
|
57
57
|
self.update(config)
|
58
58
|
self._validate(schema_src=PIFACE_SCHEMA_SRC)
|
59
|
-
if "path" in self:
|
60
|
-
warn(
|
61
|
-
message="'path' specification as a top-level pipeline "
|
62
|
-
"interface key is deprecated and will be removed with "
|
63
|
-
"the next release. Please use 'paths' section "
|
64
|
-
"from now on.",
|
65
|
-
category=DeprecationWarning,
|
66
|
-
)
|
67
|
-
self._expand_paths(["path"])
|
68
59
|
self._expand_paths(["compute", "dynamic_variables_script_path"])
|
69
60
|
|
70
61
|
@property
|
looper/project.py
CHANGED
@@ -413,10 +413,12 @@ class Project(peppyProject):
|
|
413
413
|
pipestat_config_path = self._check_for_existing_pipestat_config(piface)
|
414
414
|
|
415
415
|
if not pipestat_config_path:
|
416
|
-
self._create_pipestat_config(piface)
|
416
|
+
self._create_pipestat_config(piface, pipeline_type)
|
417
417
|
else:
|
418
418
|
piface.psm = PipestatManager(
|
419
|
-
config_file=pipestat_config_path,
|
419
|
+
config_file=pipestat_config_path,
|
420
|
+
multi_pipelines=True,
|
421
|
+
pipeline_type="sample",
|
420
422
|
)
|
421
423
|
|
422
424
|
elif pipeline_type == PipelineLevel.PROJECT.value:
|
@@ -426,10 +428,12 @@ class Project(peppyProject):
|
|
426
428
|
)
|
427
429
|
|
428
430
|
if not pipestat_config_path:
|
429
|
-
self._create_pipestat_config(prj_piface)
|
431
|
+
self._create_pipestat_config(prj_piface, pipeline_type)
|
430
432
|
else:
|
431
433
|
prj_piface.psm = PipestatManager(
|
432
|
-
config_file=pipestat_config_path,
|
434
|
+
config_file=pipestat_config_path,
|
435
|
+
multi_pipelines=True,
|
436
|
+
pipeline_type="project",
|
433
437
|
)
|
434
438
|
else:
|
435
439
|
_LOGGER.error(
|
@@ -469,7 +473,7 @@ class Project(peppyProject):
|
|
469
473
|
else:
|
470
474
|
return None
|
471
475
|
|
472
|
-
def _create_pipestat_config(self, piface):
|
476
|
+
def _create_pipestat_config(self, piface, pipeline_type):
|
473
477
|
"""
|
474
478
|
Each piface needs its own config file and associated psm
|
475
479
|
"""
|
@@ -512,8 +516,6 @@ class Project(peppyProject):
|
|
512
516
|
pipestat_config_dict.update({"pipeline_name": piface.data["pipeline_name"]})
|
513
517
|
else:
|
514
518
|
pipeline_name = None
|
515
|
-
if "pipeline_type" in piface.data:
|
516
|
-
pipestat_config_dict.update({"pipeline_type": piface.data["pipeline_type"]})
|
517
519
|
|
518
520
|
# Warn user if there is a mismatch in pipeline_names from sources!!!
|
519
521
|
if pipeline_name != output_schema_pipeline_name:
|
@@ -9,12 +9,20 @@ properties:
|
|
9
9
|
type: string
|
10
10
|
enum: ["project", "sample"]
|
11
11
|
description: "type of the pipeline, either 'project' or 'sample'"
|
12
|
-
|
13
|
-
type:
|
14
|
-
description: "
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
sample_interface:
|
13
|
+
type: object
|
14
|
+
description: "Section that defines compute environment settings"
|
15
|
+
properties:
|
16
|
+
command_template:
|
17
|
+
type: string
|
18
|
+
description: "Jinja2-like template to construct the command to run"
|
19
|
+
project_interface:
|
20
|
+
type: object
|
21
|
+
description: "Section that defines compute environment settings"
|
22
|
+
properties:
|
23
|
+
command_template:
|
24
|
+
type: string
|
25
|
+
description: "Jinja2-like template to construct the command to run"
|
18
26
|
compute:
|
19
27
|
type: object
|
20
28
|
description: "Section that defines compute environment settings"
|
looper/utils.py
CHANGED
@@ -20,7 +20,9 @@ from yacman import load_yaml
|
|
20
20
|
|
21
21
|
from .const import *
|
22
22
|
from .command_models.commands import SUPPORTED_COMMANDS
|
23
|
-
from .exceptions import MisconfigurationException
|
23
|
+
from .exceptions import MisconfigurationException, PipelineInterfaceConfigError
|
24
|
+
from rich.console import Console
|
25
|
+
from rich.pretty import pprint
|
24
26
|
|
25
27
|
_LOGGER = getLogger(__name__)
|
26
28
|
|
@@ -273,7 +275,7 @@ def enrich_args_via_cfg(
|
|
273
275
|
"""
|
274
276
|
cfg_args_all = (
|
275
277
|
_get_subcommand_args(subcommand_name, parser_args)
|
276
|
-
if os.path.exists(parser_args.
|
278
|
+
if os.path.exists(parser_args.pep_config)
|
277
279
|
else dict()
|
278
280
|
)
|
279
281
|
|
@@ -360,7 +362,7 @@ def _get_subcommand_args(subcommand_name, parser_args):
|
|
360
362
|
"""
|
361
363
|
args = dict()
|
362
364
|
cfg = peppyProject(
|
363
|
-
parser_args.
|
365
|
+
parser_args.pep_config,
|
364
366
|
defer_samples_creation=True,
|
365
367
|
amendments=parser_args.amend,
|
366
368
|
)
|
@@ -402,40 +404,66 @@ def _get_subcommand_args(subcommand_name, parser_args):
|
|
402
404
|
return args
|
403
405
|
|
404
406
|
|
405
|
-
def init_generic_pipeline():
|
407
|
+
def init_generic_pipeline(pipelinepath: Optional[str] = None):
|
406
408
|
"""
|
407
409
|
Create generic pipeline interface
|
408
410
|
"""
|
409
|
-
|
410
|
-
os.makedirs("pipeline")
|
411
|
-
except FileExistsError:
|
412
|
-
pass
|
411
|
+
console = Console()
|
413
412
|
|
414
413
|
# Destination one level down from CWD in pipeline folder
|
415
|
-
|
414
|
+
if not pipelinepath:
|
415
|
+
try:
|
416
|
+
os.makedirs("pipeline")
|
417
|
+
except FileExistsError:
|
418
|
+
pass
|
419
|
+
|
420
|
+
dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_PIPELINE)
|
421
|
+
else:
|
422
|
+
if os.path.isabs(pipelinepath):
|
423
|
+
dest_file = pipelinepath
|
424
|
+
else:
|
425
|
+
dest_file = os.path.join(os.getcwd(), os.path.relpath(pipelinepath))
|
426
|
+
try:
|
427
|
+
os.makedirs(os.path.dirname(dest_file))
|
428
|
+
except FileExistsError:
|
429
|
+
pass
|
416
430
|
|
417
431
|
# Create Generic Pipeline Interface
|
418
432
|
generic_pipeline_dict = {
|
419
433
|
"pipeline_name": "default_pipeline_name",
|
420
|
-
"pipeline_type": "sample",
|
421
434
|
"output_schema": "output_schema.yaml",
|
422
|
-
"var_templates": {"pipeline": "{looper.piface_dir}/
|
423
|
-
"
|
424
|
-
|
435
|
+
"var_templates": {"pipeline": "{looper.piface_dir}/count_lines.sh"},
|
436
|
+
"sample_interface": {
|
437
|
+
"command_template": "{pipeline.var_templates.pipeline} {sample.file} "
|
438
|
+
"--output-parent {looper.sample_output_folder}"
|
439
|
+
},
|
425
440
|
}
|
426
441
|
|
442
|
+
console.rule(f"\n[magenta]Pipeline Interface[/magenta]")
|
427
443
|
# Write file
|
428
444
|
if not os.path.exists(dest_file):
|
445
|
+
pprint(generic_pipeline_dict, expand_all=True)
|
446
|
+
|
429
447
|
with open(dest_file, "w") as file:
|
430
448
|
yaml.dump(generic_pipeline_dict, file)
|
431
|
-
|
449
|
+
|
450
|
+
console.print(
|
451
|
+
f"Pipeline interface successfully created at: [yellow]{dest_file}[/yellow]"
|
452
|
+
)
|
453
|
+
|
432
454
|
else:
|
433
|
-
print(
|
434
|
-
f"Pipeline interface file already exists `{dest_file}
|
455
|
+
console.print(
|
456
|
+
f"Pipeline interface file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
|
435
457
|
)
|
436
458
|
|
437
459
|
# Create Generic Output Schema
|
438
|
-
|
460
|
+
if not pipelinepath:
|
461
|
+
dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_OUTPUT_SCHEMA)
|
462
|
+
else:
|
463
|
+
dest_file = os.path.join(
|
464
|
+
os.path.dirname(dest_file), LOOPER_GENERIC_OUTPUT_SCHEMA
|
465
|
+
)
|
466
|
+
|
439
467
|
generic_output_schema_dict = {
|
440
468
|
"pipeline_name": "default_pipeline_name",
|
441
469
|
"samples": {
|
@@ -445,27 +473,45 @@ def init_generic_pipeline():
|
|
445
473
|
}
|
446
474
|
},
|
447
475
|
}
|
476
|
+
|
477
|
+
console.rule(f"\n[magenta]Output Schema[/magenta]")
|
448
478
|
# Write file
|
449
479
|
if not os.path.exists(dest_file):
|
480
|
+
pprint(generic_output_schema_dict, expand_all=True)
|
450
481
|
with open(dest_file, "w") as file:
|
451
482
|
yaml.dump(generic_output_schema_dict, file)
|
452
|
-
print(
|
483
|
+
console.print(
|
484
|
+
f"Output schema successfully created at: [yellow]{dest_file}[/yellow]"
|
485
|
+
)
|
453
486
|
else:
|
454
|
-
print(
|
487
|
+
console.print(
|
488
|
+
f"Output schema file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
|
489
|
+
)
|
455
490
|
|
491
|
+
console.rule(f"\n[magenta]Example Pipeline Shell Script[/magenta]")
|
456
492
|
# Create Generic countlines.sh
|
457
|
-
|
493
|
+
|
494
|
+
if not pipelinepath:
|
495
|
+
dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_COUNT_LINES)
|
496
|
+
else:
|
497
|
+
dest_file = os.path.join(os.path.dirname(dest_file), LOOPER_GENERIC_COUNT_LINES)
|
498
|
+
|
458
499
|
shell_code = """#!/bin/bash
|
459
500
|
linecount=`wc -l $1 | sed -E 's/^[[:space:]]+//' | cut -f1 -d' '`
|
460
501
|
pipestat report -r $2 -i 'number_of_lines' -v $linecount -c $3
|
461
502
|
echo "Number of lines: $linecount"
|
462
503
|
"""
|
463
504
|
if not os.path.exists(dest_file):
|
505
|
+
console.print(shell_code)
|
464
506
|
with open(dest_file, "w") as file:
|
465
507
|
file.write(shell_code)
|
466
|
-
print(
|
508
|
+
console.print(
|
509
|
+
f"count_lines.sh successfully created at: [yellow]{dest_file}[/yellow]"
|
510
|
+
)
|
467
511
|
else:
|
468
|
-
print(
|
512
|
+
console.print(
|
513
|
+
f"count_lines.sh file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
|
514
|
+
)
|
469
515
|
|
470
516
|
return True
|
471
517
|
|
@@ -500,8 +546,14 @@ def initiate_looper_config(
|
|
500
546
|
:param bool force: whether the existing file should be overwritten
|
501
547
|
:return bool: whether the file was initialized
|
502
548
|
"""
|
549
|
+
console = Console()
|
550
|
+
console.clear()
|
551
|
+
console.rule(f"\n[magenta]Looper initialization[/magenta]")
|
552
|
+
|
503
553
|
if os.path.exists(looper_config_path) and not force:
|
504
|
-
print(
|
554
|
+
console.print(
|
555
|
+
f"[red]Can't initialize, file exists:[/red] [yellow]{looper_config_path}[/yellow]"
|
556
|
+
)
|
505
557
|
return False
|
506
558
|
|
507
559
|
if pep_path:
|
@@ -521,24 +573,171 @@ def initiate_looper_config(
|
|
521
573
|
if not output_dir:
|
522
574
|
output_dir = "."
|
523
575
|
|
576
|
+
if sample_pipeline_interfaces is None or sample_pipeline_interfaces == []:
|
577
|
+
sample_pipeline_interfaces = "pipeline_interface1.yaml"
|
578
|
+
|
579
|
+
if project_pipeline_interfaces is None or project_pipeline_interfaces == []:
|
580
|
+
project_pipeline_interfaces = "pipeline_interface2.yaml"
|
581
|
+
|
524
582
|
looper_config_dict = {
|
525
583
|
"pep_config": os.path.relpath(pep_path),
|
526
584
|
"output_dir": output_dir,
|
527
|
-
"pipeline_interfaces":
|
528
|
-
|
529
|
-
|
530
|
-
|
585
|
+
"pipeline_interfaces": [
|
586
|
+
sample_pipeline_interfaces,
|
587
|
+
project_pipeline_interfaces,
|
588
|
+
],
|
531
589
|
}
|
532
590
|
|
591
|
+
pprint(looper_config_dict, expand_all=True)
|
592
|
+
|
533
593
|
with open(looper_config_path, "w") as dotfile:
|
534
594
|
yaml.dump(looper_config_dict, dotfile)
|
535
|
-
print(
|
595
|
+
console.print(
|
596
|
+
f"Initialized looper config file: [yellow]{looper_config_path}[/yellow]"
|
597
|
+
)
|
598
|
+
|
599
|
+
return True
|
600
|
+
|
601
|
+
|
602
|
+
def looper_config_tutorial():
|
603
|
+
"""
|
604
|
+
Prompt a user through configuring a .looper.yaml file for a new project.
|
605
|
+
|
606
|
+
:return bool: whether the file was initialized
|
607
|
+
"""
|
608
|
+
|
609
|
+
console = Console()
|
610
|
+
console.clear()
|
611
|
+
console.rule(f"\n[magenta]Looper initialization[/magenta]")
|
612
|
+
|
613
|
+
looper_cfg_path = ".looper.yaml" # not changeable
|
614
|
+
|
615
|
+
if os.path.exists(looper_cfg_path):
|
616
|
+
console.print(
|
617
|
+
f"[bold red]File exists at '{looper_cfg_path}'. Delete it to re-initialize. \n[/bold red]"
|
618
|
+
)
|
619
|
+
raise SystemExit
|
620
|
+
|
621
|
+
cfg = {}
|
622
|
+
|
623
|
+
console.print(
|
624
|
+
"This utility will walk you through creating a [yellow].looper.yaml[/yellow] file."
|
625
|
+
)
|
626
|
+
console.print("See [yellow]`looper init --help`[/yellow] for details.")
|
627
|
+
console.print("Use [yellow]`looper run`[/yellow] afterwards to run the pipeline.")
|
628
|
+
console.print("Press [yellow]^C[/yellow] at any time to quit.\n")
|
629
|
+
|
630
|
+
console.input("> ... ")
|
631
|
+
|
632
|
+
DEFAULTS = { # What you get if you just press enter
|
633
|
+
"pep_config": "databio/example",
|
634
|
+
"output_dir": "results",
|
635
|
+
"piface_path": "pipeline/pipeline_interface.yaml",
|
636
|
+
"project_name": os.path.basename(os.getcwd()),
|
637
|
+
}
|
638
|
+
|
639
|
+
creating = True
|
640
|
+
|
641
|
+
while creating:
|
642
|
+
cfg["project_name"] = (
|
643
|
+
console.input(
|
644
|
+
f"Project name: [yellow]({DEFAULTS['project_name']})[/yellow] >"
|
645
|
+
)
|
646
|
+
or DEFAULTS["project_name"]
|
647
|
+
)
|
648
|
+
|
649
|
+
cfg["pep_config"] = (
|
650
|
+
console.input(
|
651
|
+
f"Registry path or file path to PEP: [yellow]({DEFAULTS['pep_config']})[/yellow] >"
|
652
|
+
)
|
653
|
+
or DEFAULTS["pep_config"]
|
654
|
+
)
|
655
|
+
|
656
|
+
if not os.path.exists(cfg["pep_config"]):
|
657
|
+
console.print(
|
658
|
+
f"Warning: PEP file does not exist at [yellow]'{cfg['pep_config']}[/yellow]'"
|
659
|
+
)
|
660
|
+
|
661
|
+
cfg["output_dir"] = (
|
662
|
+
console.input(
|
663
|
+
f"Path to output directory: [yellow]({DEFAULTS['output_dir']})[/yellow] >"
|
664
|
+
)
|
665
|
+
or DEFAULTS["output_dir"]
|
666
|
+
)
|
667
|
+
|
668
|
+
add_more_pifaces = True
|
669
|
+
piface_paths = []
|
670
|
+
while add_more_pifaces:
|
671
|
+
piface_path = (
|
672
|
+
console.input(
|
673
|
+
"Add each path to a pipeline interface: [yellow](pipeline_interface.yaml)[/yellow] >"
|
674
|
+
)
|
675
|
+
or None
|
676
|
+
)
|
677
|
+
if piface_path is None:
|
678
|
+
if piface_paths == []:
|
679
|
+
piface_paths.append(DEFAULTS["piface_path"])
|
680
|
+
add_more_pifaces = False
|
681
|
+
else:
|
682
|
+
piface_paths.append(piface_path)
|
683
|
+
|
684
|
+
console.print("\n")
|
685
|
+
|
686
|
+
console.print(
|
687
|
+
f"""\
|
688
|
+
[yellow]pep_config:[/yellow] {cfg['pep_config']}
|
689
|
+
[yellow]output_dir:[/yellow] {cfg['output_dir']}
|
690
|
+
[yellow]pipeline_interfaces:[/yellow]
|
691
|
+
- {piface_paths}
|
692
|
+
"""
|
693
|
+
)
|
694
|
+
|
695
|
+
console.print(
|
696
|
+
"[bold]Does this look good?[/bold] [bold green]Y[/bold green]/[red]n[/red]..."
|
697
|
+
)
|
698
|
+
selection = None
|
699
|
+
while selection not in ["y", "n"]:
|
700
|
+
selection = console.input("\nSelection: ").lower().strip()
|
701
|
+
if selection == "n":
|
702
|
+
console.print("Starting over...")
|
703
|
+
pass
|
704
|
+
if selection == "y":
|
705
|
+
creating = False
|
706
|
+
|
707
|
+
for piface_path in piface_paths:
|
708
|
+
if not os.path.exists(piface_path):
|
709
|
+
console.print(
|
710
|
+
f"[bold red]Warning:[/bold red] File does not exist at [yellow]{piface_path}[/yellow]"
|
711
|
+
)
|
712
|
+
console.print(
|
713
|
+
"Do you wish to initialize a generic pipeline interface? [bold green]Y[/bold green]/[red]n[/red]..."
|
714
|
+
)
|
715
|
+
selection = None
|
716
|
+
while selection not in ["y", "n"]:
|
717
|
+
selection = console.input("\nSelection: ").lower().strip()
|
718
|
+
if selection == "n":
|
719
|
+
console.print(
|
720
|
+
"Use command [yellow]`looper init_piface`[/yellow] to create a generic pipeline interface."
|
721
|
+
)
|
722
|
+
if selection == "y":
|
723
|
+
init_generic_pipeline(pipelinepath=piface_path)
|
724
|
+
|
725
|
+
console.print(f"Writing config file to [yellow]{looper_cfg_path}[/yellow]")
|
726
|
+
|
727
|
+
looper_config_dict = {}
|
728
|
+
looper_config_dict["pep_config"] = cfg["pep_config"]
|
729
|
+
looper_config_dict["output_dir"] = cfg["output_dir"]
|
730
|
+
looper_config_dict["pipeline_interfaces"] = [piface_paths]
|
731
|
+
|
732
|
+
with open(looper_cfg_path, "w") as fp:
|
733
|
+
yaml.dump(looper_config_dict, fp)
|
734
|
+
|
536
735
|
return True
|
537
736
|
|
538
737
|
|
539
738
|
def determine_pipeline_type(piface_path: str, looper_config_path: str):
|
540
739
|
"""
|
541
|
-
Read pipeline interface from disk and determine if
|
740
|
+
Read pipeline interface from disk and determine if it contains "sample_interface", "project_interface" or both
|
542
741
|
|
543
742
|
|
544
743
|
:param str piface_path: path to pipeline_interface
|
@@ -558,9 +757,18 @@ def determine_pipeline_type(piface_path: str, looper_config_path: str):
|
|
558
757
|
except FileNotFoundError:
|
559
758
|
return None, None
|
560
759
|
|
561
|
-
|
760
|
+
pipeline_types = []
|
761
|
+
if piface_dict.get("sample_interface", None):
|
762
|
+
pipeline_types.append(PipelineLevel.SAMPLE.value)
|
763
|
+
if piface_dict.get("project_interface", None):
|
764
|
+
pipeline_types.append(PipelineLevel.PROJECT.value)
|
765
|
+
|
766
|
+
if pipeline_types == []:
|
767
|
+
raise PipelineInterfaceConfigError(
|
768
|
+
f"sample_interface and/or project_interface must be defined in each pipeline interface."
|
769
|
+
)
|
562
770
|
|
563
|
-
return
|
771
|
+
return pipeline_types, piface_path
|
564
772
|
|
565
773
|
|
566
774
|
def read_looper_config_file(looper_config_path: str) -> dict:
|
@@ -579,15 +787,8 @@ def read_looper_config_file(looper_config_path: str) -> dict:
|
|
579
787
|
dp_data = yaml.safe_load(dotfile)
|
580
788
|
|
581
789
|
if PEP_CONFIG_KEY in dp_data:
|
582
|
-
# Looper expects the config path to live at looper.config_file
|
583
|
-
# However, user may wish to access the pep at looper.pep_config
|
584
|
-
return_dict[PEP_CONFIG_FILE_KEY] = dp_data[PEP_CONFIG_KEY]
|
585
790
|
return_dict[PEP_CONFIG_KEY] = dp_data[PEP_CONFIG_KEY]
|
586
791
|
|
587
|
-
# TODO: delete it in looper 2.0
|
588
|
-
elif DOTFILE_CFG_PTH_KEY in dp_data:
|
589
|
-
return_dict[PEP_CONFIG_FILE_KEY] = dp_data[DOTFILE_CFG_PTH_KEY]
|
590
|
-
|
591
792
|
else:
|
592
793
|
raise MisconfigurationException(
|
593
794
|
f"Looper dotfile ({looper_config_path}) is missing '{PEP_CONFIG_KEY}' key"
|
@@ -613,36 +814,25 @@ def read_looper_config_file(looper_config_path: str) -> dict:
|
|
613
814
|
|
614
815
|
dp_data.setdefault(PIPELINE_INTERFACES_KEY, {})
|
615
816
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
"project"
|
817
|
+
all_pipeline_interfaces = dp_data.get(PIPELINE_INTERFACES_KEY)
|
818
|
+
|
819
|
+
sample_pifaces = []
|
820
|
+
project_pifaces = []
|
821
|
+
if isinstance(all_pipeline_interfaces, str):
|
822
|
+
all_pipeline_interfaces = [all_pipeline_interfaces]
|
823
|
+
for piface in all_pipeline_interfaces:
|
824
|
+
pipeline_types, piface_path = determine_pipeline_type(
|
825
|
+
piface, looper_config_path
|
626
826
|
)
|
627
|
-
|
628
|
-
|
629
|
-
all_pipeline_interfaces = dp_data.get(PIPELINE_INTERFACES_KEY)
|
630
|
-
sample_pifaces = []
|
631
|
-
project_pifaces = []
|
632
|
-
if isinstance(all_pipeline_interfaces, str):
|
633
|
-
all_pipeline_interfaces = [all_pipeline_interfaces]
|
634
|
-
for piface in all_pipeline_interfaces:
|
635
|
-
pipeline_type, piface_path = determine_pipeline_type(
|
636
|
-
piface, looper_config_path
|
637
|
-
)
|
638
|
-
if pipeline_type == PipelineLevel.SAMPLE.value:
|
827
|
+
if pipeline_types is not None:
|
828
|
+
if PipelineLevel.SAMPLE.value in pipeline_types:
|
639
829
|
sample_pifaces.append(piface_path)
|
640
|
-
|
830
|
+
if PipelineLevel.PROJECT.value in pipeline_types:
|
641
831
|
project_pifaces.append(piface_path)
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
832
|
+
if len(sample_pifaces) > 0:
|
833
|
+
return_dict[SAMPLE_PL_ARG] = sample_pifaces
|
834
|
+
if len(project_pifaces) > 0:
|
835
|
+
return_dict[PROJECT_PL_ARG] = project_pifaces
|
646
836
|
|
647
837
|
else:
|
648
838
|
_LOGGER.warning(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: looper
|
3
|
-
Version:
|
3
|
+
Version: 2.0.0a1
|
4
4
|
Summary: A pipeline submission engine that parses sample inputs and submits pipelines for each sample.
|
5
5
|
Home-page: https://github.com/pepkit/looper
|
6
6
|
Author: Nathan Sheffield, Vince Reuter, Michal Stolarczyk, Johanna Klughammer, Andre Rendeiro
|
@@ -23,7 +23,7 @@ Requires-Dist: logmuse >=0.2.0
|
|
23
23
|
Requires-Dist: pandas >=2.0.2
|
24
24
|
Requires-Dist: pephubclient >=0.4.0
|
25
25
|
Requires-Dist: pipestat >=0.9.2
|
26
|
-
Requires-Dist: peppy
|
26
|
+
Requires-Dist: peppy >=0.40.2
|
27
27
|
Requires-Dist: pyyaml >=3.12
|
28
28
|
Requires-Dist: rich >=9.10.0
|
29
29
|
Requires-Dist: ubiquerg >=0.8.1a1
|
@@ -1,24 +1,24 @@
|
|
1
1
|
looper/__init__.py,sha256=f_z9YY4ibOk7eyWoaViH_VaCXMlPQeiftbnibSFj-3E,1333
|
2
2
|
looper/__main__.py,sha256=OOCmI-dPUvInnJHkHNMf54cblNJ3Yl9ELOwZcfOXmD8,240
|
3
|
-
looper/_version.py,sha256=
|
3
|
+
looper/_version.py,sha256=wJ2KiFRb6QXaKu1tfxwKmo9CE2xP4f3BkEUYoIJeUkQ,121
|
4
4
|
looper/cli_divvy.py,sha256=J07x83sqC4jJeu3_yS6KOARPWmwKGAV7JvN33T5zDac,5907
|
5
|
-
looper/cli_pydantic.py,sha256=
|
6
|
-
looper/conductor.py,sha256=
|
5
|
+
looper/cli_pydantic.py,sha256=saAD0dwizLi_VajVKElQmYRqtn0Yyl_r712znxZMxRQ,14209
|
6
|
+
looper/conductor.py,sha256=oNfqANA2tIhQTAQFxd-rQ4ccPA60D933EGo3rfbJGFo,35061
|
7
7
|
looper/const.py,sha256=OscEELQsyLKlSrmwuXfyLRwpAUJUEpGD2UxBeLJDXgw,8703
|
8
8
|
looper/divvy.py,sha256=5x8hV1lT5tEQdAUtVjn0rNwYnJroNij0RyDn-wHf4QE,15251
|
9
9
|
looper/exceptions.py,sha256=r6SKKt-m8CXQnXGDnuiwoA6zBJhIZflygBKjX4RCloI,3419
|
10
10
|
looper/looper.py,sha256=ZWTulMz6NobnYFUjev513TJwXqknrb4_gZrV-a_fT9g,30041
|
11
11
|
looper/parser_types.py,sha256=d3FHt54f9jo9VZMr5SQkbghcAdABqiYZW2JBGO5EBnw,2327
|
12
|
-
looper/pipeline_interface.py,sha256=
|
12
|
+
looper/pipeline_interface.py,sha256=mN4-XICyZzuVLTOq3b0ijppYe6ib_Ljlyf6KxZCJh2A,14537
|
13
13
|
looper/plugins.py,sha256=MaMdPmK9U_4FkNJE5kccohBbY1i2qj1NTEucubFOJek,5747
|
14
14
|
looper/processed_project.py,sha256=jZxoMYafvr-OHFxylc5ivGty1VwXBZhl0kgoFkY-174,9837
|
15
|
-
looper/project.py,sha256=
|
16
|
-
looper/utils.py,sha256=
|
15
|
+
looper/project.py,sha256=SFHdi58eRBWtye5lUFhwzBcG7ejrMurmDzmkrC3XAic,34339
|
16
|
+
looper/utils.py,sha256=VXKaEYXW0XEQNy__hNBv42i-sz5qB7QiwpXzhXs_3Wk,39556
|
17
17
|
looper/command_models/DEVELOPER.md,sha256=eRxnrO-vqNJjExzamXKEq5wr_-Zw6PQEwkS9RPinYrk,2775
|
18
18
|
looper/command_models/README.md,sha256=3RGegeZlTZYnhcHXRu6bdI_81WZom2q7QYMV-KGYY7U,588
|
19
19
|
looper/command_models/__init__.py,sha256=6QWC2TewowEL7dATli5YpMmFWuXaLEPktofJCXkYUBI,187
|
20
|
-
looper/command_models/arguments.py,sha256=
|
21
|
-
looper/command_models/commands.py,sha256=
|
20
|
+
looper/command_models/arguments.py,sha256=sRrJWCSQmnjGLnOo-Wl6_PnvTZz8VIWII79svI3BqFk,8653
|
21
|
+
looper/command_models/commands.py,sha256=EvKyjNdUBspXnOUMprxIY0C4VPka2PBj8CJgtd5Ya9w,9680
|
22
22
|
looper/default_config/divvy_config.yaml,sha256=wK5kLDGBV2wwoyqg2rl3X8SXjds4x0mwBUjUzF1Ln7g,1705
|
23
23
|
looper/default_config/divvy_templates/localhost_bulker_template.sub,sha256=yn5VB9Brt7Hck9LT17hD2o8Kn-76gYJQk_A-8C1Gr4k,164
|
24
24
|
looper/default_config/divvy_templates/localhost_docker_template.sub,sha256=XRr7AlR7-TP1L3hyBMfka_RgWRL9vzOlS5Kd1xSNwT0,183
|
@@ -57,12 +57,12 @@ looper/jinja_templates_old/status.html,sha256=FBH2hqw3tVJgmR1kY3Cz5SQRHmnH6JqaDL
|
|
57
57
|
looper/jinja_templates_old/status_table.html,sha256=VbHax7cGENIfVnU-O9p2ELSAJIONJ41m-mHJBy4r4dQ,3271
|
58
58
|
looper/jinja_templates_old/status_table_no_links.html,sha256=vrPNCKen-yrkM_dg13f0yCVyeJHCQVNNY_bchUW_sVY,2396
|
59
59
|
looper/schemas/divvy_config_schema.yaml,sha256=7GJfKLc3VX4RGjHnOE1zxwsHXhj_ur9za6dKdfJTFkc,450
|
60
|
-
looper/schemas/pipeline_interface_schema_generic.yaml,sha256=
|
60
|
+
looper/schemas/pipeline_interface_schema_generic.yaml,sha256=3YfKFyRUIwxG41FEidR1dXe9IU6ye51LSUBfSpmMuss,1773
|
61
61
|
looper/schemas/pipeline_interface_schema_project.yaml,sha256=-ZWyA0lKXWik3obuLNVk3IsAZYfbLVbCDvJnD-Fcluo,1567
|
62
62
|
looper/schemas/pipeline_interface_schema_sample.yaml,sha256=x0OwVnijJpvm50DscvvJujdK4UAI7d71pqVemQS-D-0,1564
|
63
|
-
looper-
|
64
|
-
looper-
|
65
|
-
looper-
|
66
|
-
looper-
|
67
|
-
looper-
|
68
|
-
looper-
|
63
|
+
looper-2.0.0a1.dist-info/LICENSE.txt,sha256=oB6ZGDa4kcznznJKJsLLFFcOZyi8Y6e2Jv0rJozgp-I,1269
|
64
|
+
looper-2.0.0a1.dist-info/METADATA,sha256=OZPEqy9PPCjzJ110u-HazaRkaUIiuesCZMC3jpfl2vQ,1800
|
65
|
+
looper-2.0.0a1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
66
|
+
looper-2.0.0a1.dist-info/entry_points.txt,sha256=ejZpghZG3OoTK69u9rTW-yLyI6SC63bBTUb-Vw26HG4,87
|
67
|
+
looper-2.0.0a1.dist-info/top_level.txt,sha256=I0Yf7djsoQAMzwHBbDiQi9hGtq4Z41_Ma5CX8qXG8Y8,7
|
68
|
+
looper-2.0.0a1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|