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 CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.9.1"
2
- # You must change the version in parser = pydantic2_argparse.ArgumentParser in cli_pydantic.py!!!
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
- return int(
126
- not initiate_looper_config(
127
- dotfile_path(),
128
- subcommand_args.pep_config,
129
- subcommand_args.output_dir,
130
- subcommand_args.sample_pipeline_interfaces,
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
- if subcommand_args.config_file is None:
142
- looper_cfg_path = os.path.relpath(dotfile_path(), start=os.curdir)
143
- try:
144
- if subcommand_args.looper_config:
145
- looper_config_dict = read_looper_config_file(
146
- subcommand_args.looper_config
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
- looper_config_dict = read_looper_dotfile()
150
- _LOGGER.info(f"Using looper config ({looper_cfg_path}).")
151
-
152
- cli_modifiers_dict = None
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
- "This PEP configures looper through the project config. This approach is deprecated and will "
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.config_file) and os.path.exists(
195
- subcommand_args.config_file
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.config_file,
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.config_file):
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.config_file
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="1.9.1",
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
- CONFIG_FILE = Argument(
166
- name="config_file",
167
- default=(str, None),
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.CONFIG_FILE.value,
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
- templ = self.pl_iface["command_template"]
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
@@ -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, multi_pipelines=True
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, multi_pipelines=True
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
- command_template:
13
- type: string
14
- description: "Jinja2-like template to construct the command to run"
15
- path:
16
- type: string
17
- description: "path to the pipeline program. Relative to pipeline interface file or absolute."
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.config_file)
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.config_file,
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
- try:
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
- dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_PIPELINE)
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}/pipeline.sh"},
423
- "command_template": "{pipeline.var_templates.pipeline} {sample.file} "
424
- "--output-parent {looper.sample_output_folder}",
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
- print(f"Pipeline interface successfully created at: {dest_file}")
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}`. Skipping creation.."
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
- dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_OUTPUT_SCHEMA)
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(f"Output schema successfully created at: {dest_file}")
483
+ console.print(
484
+ f"Output schema successfully created at: [yellow]{dest_file}[/yellow]"
485
+ )
453
486
  else:
454
- print(f"Output schema file already exists `{dest_file}`. Skipping creation..")
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
- dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_COUNT_LINES)
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(f"count_lines.sh successfully created at: {dest_file}")
508
+ console.print(
509
+ f"count_lines.sh successfully created at: [yellow]{dest_file}[/yellow]"
510
+ )
467
511
  else:
468
- print(f"count_lines.sh file already exists `{dest_file}`. Skipping creation..")
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(f"Can't initialize, file exists: {looper_config_path}")
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
- "sample": sample_pipeline_interfaces,
529
- "project": project_pipeline_interfaces,
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(f"Initialized looper config file: {looper_config_path}")
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 pipeline type is sample or project-level
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
- pipeline_type = piface_dict.get("pipeline_type", None)
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 pipeline_type, piface_path
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
- if isinstance(dp_data.get(PIPELINE_INTERFACES_KEY), dict) and (
617
- dp_data.get(PIPELINE_INTERFACES_KEY).get("sample")
618
- or dp_data.get(PIPELINE_INTERFACES_KEY).get("project")
619
- ):
620
- # Support original nesting of pipeline interfaces under "sample" and "project"
621
- return_dict[SAMPLE_PL_ARG] = dp_data.get(PIPELINE_INTERFACES_KEY).get(
622
- "sample"
623
- )
624
- return_dict[PROJECT_PL_ARG] = dp_data.get(PIPELINE_INTERFACES_KEY).get(
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
- else:
628
- # infer pipeline type based from interface instead of nested keys: https://github.com/pepkit/looper/issues/465
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
- elif pipeline_type == PipelineLevel.PROJECT.value:
830
+ if PipelineLevel.PROJECT.value in pipeline_types:
641
831
  project_pifaces.append(piface_path)
642
- if len(sample_pifaces) > 0:
643
- return_dict[SAMPLE_PL_ARG] = sample_pifaces
644
- if len(project_pifaces) > 0:
645
- return_dict[PROJECT_PL_ARG] = project_pifaces
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: 1.9.1
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 <=0.40.2,>=0.40.0
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=Ghy5rA1bB9z5xOMUOvtP6NkyAM_ZJj_GadLFiB5IJIQ,120
3
+ looper/_version.py,sha256=wJ2KiFRb6QXaKu1tfxwKmo9CE2xP4f3BkEUYoIJeUkQ,121
4
4
  looper/cli_divvy.py,sha256=J07x83sqC4jJeu3_yS6KOARPWmwKGAV7JvN33T5zDac,5907
5
- looper/cli_pydantic.py,sha256=xDYj1ABJNiHg2RJi6i5LH2DhPRq0vz5Q9DBVNk1IlK8,14017
6
- looper/conductor.py,sha256=DSpil080IYYqu-76ms25jrNSTmog0449tPXn0nK38Dw,34786
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=dBXwsU59vR4qmUC59Bt3iM2187mXSDdysMNOhf63pPw,14922
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=svkCChwpbFBSJZdYXWcOol0GZnWNWaw32yyye2ajkXw,34279
16
- looper/utils.py,sha256=KkXQ6igvuuWBhb-q3TzCUYf39aoWD9CGJ06f5zhVAyw,33799
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=emK7gc_fVgrSPHE2cShxJX05VrgOEn4H7szU8DBev7Q,8808
21
- looper/command_models/commands.py,sha256=WieHeBGkZQlKFqUph6GEpd12dIUmJNJ4lLMgN2xeZJA,9723
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=D16Rkpj03H9WnvA_N18iNU-hH_HwOuyESJ8Hk5hZSXc,1518
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-1.9.1.dist-info/LICENSE.txt,sha256=oB6ZGDa4kcznznJKJsLLFFcOZyi8Y6e2Jv0rJozgp-I,1269
64
- looper-1.9.1.dist-info/METADATA,sha256=hacnQb4KXUjACK_vJRTzff7XXcILGXypa-y10ic3weU,1807
65
- looper-1.9.1.dist-info/WHEEL,sha256=YiKiUUeZQGmGJoR_0N1Y933DOBowq4AIvDe2-UIy8E4,91
66
- looper-1.9.1.dist-info/entry_points.txt,sha256=ejZpghZG3OoTK69u9rTW-yLyI6SC63bBTUb-Vw26HG4,87
67
- looper-1.9.1.dist-info/top_level.txt,sha256=I0Yf7djsoQAMzwHBbDiQi9hGtq4Z41_Ma5CX8qXG8Y8,7
68
- looper-1.9.1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.2)
2
+ Generator: setuptools (70.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5