Skip to content

Commit

Permalink
Merge pull request #508 from pepkit/dev_looper_init_walkthrough
Browse files Browse the repository at this point in the history
Polish looper init tutorial and looper init_piface
  • Loading branch information
donaldcampbelljr authored Jul 1, 2024
2 parents 2d2475b + cc1b1ad commit 636d10c
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 94 deletions.
37 changes: 28 additions & 9 deletions looper/cli_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@
read_yaml_file,
inspect_looper_config_file,
is_PEP_file_type,
looper_config_tutorial,
)

from typing import List, Tuple
from rich.console import Console


def opt_attr_pair(name: str) -> Tuple[str, str]:
Expand Down Expand Up @@ -122,17 +124,34 @@ def run_looper(args: TopLevelParser, parser: ArgumentParser, test_args=None):
sys.exit(1)

if subcommand_name == "init":
return int(
not initiate_looper_config(
dotfile_path(),
subcommand_args.pep_config,
subcommand_args.output_dir,
subcommand_args.sample_pipeline_interfaces,
subcommand_args.project_pipeline_interfaces,
subcommand_args.force_yes,
)

console = Console()
console.clear()
console.rule(f"\n[magenta]Looper initialization[/magenta]")
console.print(
"[bold]Would you like to follow a guided tutorial?[/bold] [green]Y[/green] / [red]n[/red]..."
)

selection = None
while selection not in ["y", "n"]:
selection = console.input("\nSelection: ").lower().strip()

if selection == "n":
console.clear()
return int(
not initiate_looper_config(
dotfile_path(),
subcommand_args.pep_config,
subcommand_args.output_dir,
subcommand_args.sample_pipeline_interfaces,
subcommand_args.project_pipeline_interfaces,
subcommand_args.force_yes,
)
)
else:
console.clear()
return int(looper_config_tutorial())

if subcommand_name == "init_piface":
sys.exit(int(not init_generic_pipeline()))

Expand Down
186 changes: 169 additions & 17 deletions looper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from .const import *
from .command_models.commands import SUPPORTED_COMMANDS
from .exceptions import MisconfigurationException, PipelineInterfaceConfigError
from rich.console import Console
from rich.pretty import pprint

_LOGGER = getLogger(__name__)

Expand Down Expand Up @@ -406,6 +408,8 @@ def init_generic_pipeline():
"""
Create generic pipeline interface
"""
console = Console()

try:
os.makedirs("pipeline")
except FileExistsError:
Expand All @@ -417,21 +421,26 @@ def init_generic_pipeline():
# Create Generic Pipeline Interface
generic_pipeline_dict = {
"pipeline_name": "default_pipeline_name",
"pipeline_type": "sample",
"output_schema": "output_schema.yaml",
"var_templates": {"pipeline": "{looper.piface_dir}/pipeline.sh"},
"command_template": "{pipeline.var_templates.pipeline} {sample.file} "
"--output-parent {looper.sample_output_folder}",
"var_templates": {"pipeline": "{looper.piface_dir}/count_lines.sh"},
"sample_interface": {
"command_template": "{pipeline.var_templates.pipeline} {sample.file} "
"--output-parent {looper.sample_output_folder}"
},
}

console.rule(f"\n[magenta]Pipeline Interface[/magenta]")
# Write file
if not os.path.exists(dest_file):
pprint(generic_pipeline_dict, expand_all=True)
with open(dest_file, "w") as file:
yaml.dump(generic_pipeline_dict, file)
print(f"Pipeline interface successfully created at: {dest_file}")
console.print(
f"Pipeline interface successfully created at: [yellow]{dest_file}[/yellow]"
)
else:
print(
f"Pipeline interface file already exists `{dest_file}`. Skipping creation.."
console.print(
f"Pipeline interface file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
)

# Create Generic Output Schema
Expand All @@ -445,14 +454,22 @@ def init_generic_pipeline():
}
},
}

console.rule(f"\n[magenta]Output Schema[/magenta]")
# Write file
if not os.path.exists(dest_file):
pprint(generic_output_schema_dict, expand_all=True)
with open(dest_file, "w") as file:
yaml.dump(generic_output_schema_dict, file)
print(f"Output schema successfully created at: {dest_file}")
console.print(
f"Output schema successfully created at: [yellow]{dest_file}[/yellow]"
)
else:
print(f"Output schema file already exists `{dest_file}`. Skipping creation..")
console.print(
f"Output schema file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
)

console.rule(f"\n[magenta]Example Pipeline Shell Script[/magenta]")
# Create Generic countlines.sh
dest_file = os.path.join(os.getcwd(), "pipeline", LOOPER_GENERIC_COUNT_LINES)
shell_code = """#!/bin/bash
Expand All @@ -461,11 +478,16 @@ def init_generic_pipeline():
echo "Number of lines: $linecount"
"""
if not os.path.exists(dest_file):
console.print(shell_code)
with open(dest_file, "w") as file:
file.write(shell_code)
print(f"count_lines.sh successfully created at: {dest_file}")
console.print(
f"count_lines.sh successfully created at: [yellow]{dest_file}[/yellow]"
)
else:
print(f"count_lines.sh file already exists `{dest_file}`. Skipping creation..")
console.print(
f"count_lines.sh file already exists [yellow]`{dest_file}`[/yellow]. Skipping creation.."
)

return True

Expand Down Expand Up @@ -500,8 +522,14 @@ def initiate_looper_config(
:param bool force: whether the existing file should be overwritten
:return bool: whether the file was initialized
"""
console = Console()
console.clear()
console.rule(f"\n[magenta]Looper initialization[/magenta]")

if os.path.exists(looper_config_path) and not force:
print(f"Can't initialize, file exists: {looper_config_path}")
console.print(
f"[red]Can't initialize, file exists:[/red] [yellow]{looper_config_path}[/yellow]"
)
return False

if pep_path:
Expand All @@ -521,18 +549,142 @@ def initiate_looper_config(
if not output_dir:
output_dir = "."

if sample_pipeline_interfaces is None or sample_pipeline_interfaces == []:
sample_pipeline_interfaces = "pipeline_interface1.yaml"

if project_pipeline_interfaces is None or project_pipeline_interfaces == []:
project_pipeline_interfaces = "pipeline_interface2.yaml"

looper_config_dict = {
"pep_config": os.path.relpath(pep_path),
"output_dir": output_dir,
"pipeline_interfaces": {
"sample": sample_pipeline_interfaces,
"project": project_pipeline_interfaces,
},
"pipeline_interfaces": [
sample_pipeline_interfaces,
project_pipeline_interfaces,
],
}

pprint(looper_config_dict, expand_all=True)

with open(looper_config_path, "w") as dotfile:
yaml.dump(looper_config_dict, dotfile)
print(f"Initialized looper config file: {looper_config_path}")
console.print(
f"Initialized looper config file: [yellow]{looper_config_path}[/yellow]"
)

return True


def looper_config_tutorial():
"""
Prompt a user through configuring a .looper.yaml file for a new project.
:return bool: whether the file was initialized
"""

console = Console()
console.clear()
console.rule(f"\n[magenta]Looper initialization[/magenta]")

looper_cfg_path = ".looper.yaml" # not changeable

if os.path.exists(looper_cfg_path):
console.print(
f"[bold red]File exists at '{looper_cfg_path}'. Delete it to re-initialize. \n[/bold red]"
)
raise SystemExit

cfg = {}

console.print(
"This utility will walk you through creating a [yellow].looper.yaml[/yellow] file."
)
console.print("See [yellow]`looper init --help`[/yellow] for details.")
console.print("Use [yellow]`looper run`[/yellow] afterwards to run the pipeline.")
console.print("Press [yellow]^C[/yellow] at any time to quit.\n")

console.input("> ... ")

DEFAULTS = { # What you get if you just press enter
"pep_config": "databio/example",
"output_dir": "results",
"piface_path": "pipeline/pipeline_interface.yaml",
"project_name": os.path.basename(os.getcwd()),
}

creating = True

while creating:
cfg["project_name"] = (
console.input(
f"Project name: [yellow]({DEFAULTS['project_name']})[/yellow] >"
)
or DEFAULTS["project_name"]
)

cfg["pep_config"] = (
console.input(
f"Registry path or file path to PEP: [yellow]({DEFAULTS['pep_config']})[/yellow] >"
)
or DEFAULTS["pep_config"]
)

if not os.path.exists(cfg["pep_config"]):
console.print(
f"Warning: PEP file does not exist at [yellow]'{cfg['pep_config']}[/yellow]'"
)

cfg["output_dir"] = (
console.input(
f"Path to output directory: [yellow]({DEFAULTS['output_dir']})[/yellow] >"
)
or DEFAULTS["output_dir"]
)

piface_path = (
console.input(
"Path to sample pipeline interface: [yellow](pipeline_interface.yaml)[/yellow] >"
)
or DEFAULTS["piface_path"]
)
console.print("\n")

console.print(
f"""\
[yellow]pep_config:[/yellow] {cfg['pep_config']}
[yellow]output_dir:[/yellow] {cfg['output_dir']}
[yellow]pipeline_interfaces:[/yellow]
- {piface_path}
"""
)

console.print(
"[bold]Does this look good?[/bold] [bold green]Y[/bold green]/[red]n[/red]..."
)
selection = None
while selection not in ["y", "n"]:
selection = console.input("\nSelection: ").lower().strip()
if selection == "n":
console.print("Starting over...")
pass
if selection == "y":
creating = False

if not os.path.exists(piface_path):
console.print(
f"[bold red]Warning:[/bold red] File does not exist at [yellow]{piface_path}[/yellow]\nUse command [yellow]`looper init_piface`[/yellow] to create a generic pipeline interface."
)

console.print(f"Writing config file to [yellow]{looper_cfg_path}[/yellow]")

looper_config_dict = {}
looper_config_dict["pep_config"] = cfg["pep_config"]
looper_config_dict["output_dir"] = cfg["output_dir"]
looper_config_dict["pipeline_interfaces"] = [piface_path]

with open(looper_cfg_path, "w") as fp:
yaml.dump(looper_config_dict, fp)

return True


Expand Down
68 changes: 0 additions & 68 deletions looper_init.py

This file was deleted.

3 changes: 3 additions & 0 deletions tests/smoketests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,9 @@ def test_cli_compute_overwrites_yaml_settings_spec(self, prep_temp_pep, cmd):
assert_content_not_in_any_files(subs_list, "testin_mem")


@pytest.mark.skip(
reason="This functionality requires input from the user. Causing pytest to error if run without -s flag"
)
class TestLooperConfig:

def test_init_config_file(self, prep_temp_pep):
Expand Down

0 comments on commit 636d10c

Please sign in to comment.