snk-cli 0.0.1__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.
@@ -0,0 +1,60 @@
1
+ import typer
2
+ from snk_cli.dynamic_typer import DynamicTyper
3
+ from ..workflow import Workflow
4
+ from rich.console import Console
5
+ from rich.syntax import Syntax
6
+ from pathlib import Path
7
+
8
+
9
+ class ProfileApp(DynamicTyper):
10
+ def __init__(
11
+ self,
12
+ workflow: Workflow,
13
+ ):
14
+ self.workflow = workflow
15
+ self.register_command(self.list, help="List the profiles in the workflow.")
16
+ self.register_command(
17
+ self.show, help="Show the contents of a profile."
18
+ )
19
+
20
+ def list(
21
+ self,
22
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show profiles as paths."),
23
+ ):
24
+ from rich.console import Console
25
+ from rich.table import Table
26
+ table = Table("Name", "CMD", show_header=True, show_lines=True)
27
+ if verbose:
28
+ table.add_column("Path")
29
+ for profile in self.workflow.profiles:
30
+ if verbose:
31
+ path = str(profile.resolve())
32
+ table.add_row(profile.stem, f"{self.workflow.name} profile show {profile.stem}", path)
33
+ else:
34
+ table.add_row(profile.stem, f"{self.workflow.name} profile show {profile.stem}")
35
+ console = Console()
36
+ console.print(table)
37
+
38
+
39
+
40
+ def _get_profile_path(self, name: str) -> Path:
41
+ profile = [p for p in self.workflow.profiles if p.name == name or p.stem == name]
42
+ if not profile:
43
+ self.error(f"Profile {name} not found!")
44
+ return profile[0] / "config.yaml"
45
+
46
+ def show(
47
+ self,
48
+ name: str = typer.Argument(..., help="The name of the profile."),
49
+ pretty: bool = typer.Option(
50
+ False, "--pretty", "-p", help="Pretty print the profile."
51
+ ),
52
+ ):
53
+ profile_path = self._get_profile_path(name)
54
+ profile_file_text = profile_path.read_text()
55
+ if pretty:
56
+ syntax = Syntax(profile_file_text, "yaml")
57
+ console = Console()
58
+ console.print(syntax)
59
+ else:
60
+ typer.echo(profile_file_text)
@@ -0,0 +1,471 @@
1
+ from typing import List, Optional
2
+ from pathlib import Path
3
+ import typer
4
+ from contextlib import contextmanager
5
+
6
+ from snk_cli.dynamic_typer import DynamicTyper
7
+ from snk_cli.options.option import Option
8
+ from ..workflow import Workflow
9
+ from snk_cli.utils import (
10
+ parse_config_args,
11
+ dag_filetype_callback,
12
+ check_command_available
13
+ )
14
+
15
+ from snk_cli.config.config import (
16
+ SnkConfig,
17
+ get_config_from_workflow_dir,
18
+ )
19
+
20
+
21
+ class RunApp(DynamicTyper):
22
+ def __init__(
23
+ self,
24
+ conda_prefix_dir: Path,
25
+ snk_config: SnkConfig,
26
+ singularity_prefix_dir: Path,
27
+ snakefile: Path,
28
+ workflow: Workflow,
29
+ logo: str,
30
+ verbose: bool,
31
+ dynamic_run_options: List[Option],
32
+ ):
33
+ self.conda_prefix_dir = conda_prefix_dir
34
+ self.singularity_prefix_dir = singularity_prefix_dir
35
+ self.snk_config = snk_config
36
+ self.snakefile = snakefile
37
+ self.workflow = workflow
38
+ self.verbose = verbose
39
+ self.logo = logo
40
+ self.options = dynamic_run_options
41
+
42
+ self.register_command(
43
+ self.run,
44
+ dynamic_options=self.options,
45
+ help="Run the workflow.\n\nAll unrecognized arguments are passed onto Snakemake.",
46
+ context_settings={
47
+ "allow_extra_args": True,
48
+ "ignore_unknown_options": True,
49
+ "help_option_names": ["-h", "--help"],
50
+ },
51
+ )
52
+
53
+ def _print_snakemake_help(value: bool):
54
+ """
55
+ Print the snakemake help and exit.
56
+ Args:
57
+ value (bool): If True, print the snakemake help and exit.
58
+ Side Effects:
59
+ Prints the snakemake help and exits.
60
+ Examples:
61
+ >>> CLI._print_snakemake_help(True)
62
+ """
63
+ if value:
64
+ import snakemake
65
+
66
+ snakemake.main("-h")
67
+
68
+ def run(
69
+ self,
70
+ ctx: typer.Context,
71
+ configfile: Path = typer.Option(
72
+ None,
73
+ "--config",
74
+ help="Path to snakemake config file. Overrides existing workflow configuration.",
75
+ exists=True,
76
+ dir_okay=False,
77
+ ),
78
+ resource: List[Path] = typer.Option(
79
+ [],
80
+ "--resource",
81
+ "-r",
82
+ help="Additional resources to copy from workflow directory at run time.",
83
+ ),
84
+ profile: Optional[str] = typer.Option(
85
+ None,
86
+ "--profile",
87
+ "-p",
88
+ help="Name of profile to use for configuring Snakemake.",
89
+ ),
90
+ force: bool = typer.Option(
91
+ False,
92
+ "--force",
93
+ "-f",
94
+ help="Force the execution of workflow regardless of already created output.",
95
+ ),
96
+ dry: bool = typer.Option(
97
+ False,
98
+ "--dry",
99
+ "-n",
100
+ help="Do not execute anything, and display what would be done.",
101
+ ),
102
+ lock: bool = typer.Option(
103
+ False, "--lock", "-l", help="Lock the working directory."
104
+ ),
105
+ dag: Optional[Path] = typer.Option(
106
+ None,
107
+ "--dag",
108
+ "-d",
109
+ help="Save directed acyclic graph to file. Must end in .pdf, .png or .svg",
110
+ callback=dag_filetype_callback,
111
+ ),
112
+ cores: int = typer.Option(
113
+ None,
114
+ "--cores",
115
+ "-c",
116
+ help="Set the number of cores to use. If None will use all cores.",
117
+ ),
118
+ no_conda: bool = typer.Option(
119
+ False,
120
+ "--no-conda",
121
+ help="Do not use conda environments.",
122
+ ),
123
+ keep_resources: bool = typer.Option(
124
+ False,
125
+ "--keep-resources",
126
+ help="Keep resources after pipeline completes.",
127
+ ),
128
+ keep_snakemake: bool = typer.Option(
129
+ False,
130
+ "--keep-snakemake",
131
+ help="Keep .snakemake folder after pipeline completes.",
132
+ ),
133
+ verbose: Optional[bool] = typer.Option(
134
+ False,
135
+ "--verbose",
136
+ "-v",
137
+ help="Run workflow in verbose mode.",
138
+ ),
139
+ help_snakemake: Optional[bool] = typer.Option(
140
+ False,
141
+ "--help-snakemake",
142
+ "-hs",
143
+ help="Print the snakemake help and exit.",
144
+ is_eager=True,
145
+ callback=_print_snakemake_help,
146
+ show_default=False,
147
+ ),
148
+ ):
149
+ """
150
+ Run the workflow.
151
+ Args:
152
+ configfile (Path): Path to snakemake config file. Overrides existing config and defaults.
153
+ resource (List[Path]): Additional resources to copy to workdir at run time.
154
+ keep_resources (bool): Keep resources.
155
+ cleanup_snakemake (bool): Keep .snakemake folder.
156
+ cores (int): Set the number of cores to use. If None will use all cores.
157
+ verbose (bool): Run workflow in verbose mode.
158
+ help_snakemake (bool): Print the snakemake help and exit.
159
+ Side Effects:
160
+ Runs the workflow.
161
+ Examples:
162
+ >>> CLI.run(target='my_target', configfile=Path('/path/to/config.yaml'), resource=[Path('/path/to/resource')], verbose=True)
163
+ """
164
+ import snakemake
165
+ import shutil
166
+ import sys
167
+
168
+ self.verbose = verbose
169
+ args = []
170
+ if self.snk_config.additional_snakemake_args:
171
+ if verbose:
172
+ self.log(
173
+ f"Using additional snakemake args: {' '.join(self.snk_config.additional_snakemake_args)}",
174
+ color=typer.colors.MAGENTA
175
+ )
176
+ args.extend(self.snk_config.additional_snakemake_args)
177
+ if not cores:
178
+ cores = "all"
179
+ args.extend(
180
+ [
181
+ "--rerun-incomplete",
182
+ f"--cores={cores}",
183
+ ]
184
+ )
185
+ if self.singularity_prefix_dir and "--use-singularity" in ctx.args:
186
+ # only set prefix if --use-singularity is explicitly called
187
+ args.append(f"--singularity-prefix={self.singularity_prefix_dir}")
188
+ if verbose:
189
+ self.log(f"Using singularity prefix: {self.singularity_prefix_dir}", color=typer.colors.MAGENTA)
190
+ if not self.snakefile.exists():
191
+ raise ValueError("Could not find Snakefile") # this should occur at install
192
+ else:
193
+ args.append(f"--snakefile={self.snakefile}")
194
+
195
+ if not configfile:
196
+ configfile = get_config_from_workflow_dir(self.workflow.path)
197
+ if configfile:
198
+ args.append(f"--configfile={configfile}")
199
+
200
+ if profile:
201
+ found_profile = [p for p in self.workflow.profiles if profile == p.name]
202
+ if found_profile:
203
+ profile = found_profile[0]
204
+ args.append(f"--profile={profile}")
205
+
206
+ # Set up conda frontend
207
+ conda_found = check_command_available("conda")
208
+ if not conda_found and verbose:
209
+ typer.secho(
210
+ "Conda not found! Install conda to use environments.\n",
211
+ fg=typer.colors.MAGENTA,
212
+ err=True,
213
+ )
214
+
215
+ if conda_found and self.snk_config.conda and not no_conda:
216
+ args.extend(
217
+ [
218
+ "--use-conda",
219
+ f"--conda-prefix={self.conda_prefix_dir}",
220
+ ]
221
+ )
222
+ if not check_command_available("mamba"):
223
+ if verbose:
224
+ typer.secho(
225
+ "Could not find mamba, using conda instead...",
226
+ fg=typer.colors.MAGENTA,
227
+ err=True,
228
+ )
229
+ args.append("--conda-frontend=conda")
230
+ else:
231
+ args.append("--conda-frontend=mamba")
232
+
233
+ if verbose:
234
+ args.insert(0, "--verbose")
235
+
236
+ if force:
237
+ args.append("--forceall")
238
+
239
+ if dry:
240
+ args.append("--dryrun")
241
+
242
+ if not lock:
243
+ args.append("--nolock")
244
+ targets_and_or_snakemake, config_dict_list = parse_config_args(
245
+ ctx.args, options=self.options
246
+ )
247
+ targets_and_or_snakemake = [
248
+ t.replace("--snake-", "-") for t in targets_and_or_snakemake
249
+ ]
250
+ args.extend(targets_and_or_snakemake)
251
+ configs = []
252
+ for config_dict in config_dict_list:
253
+ for key, value in config_dict.items():
254
+ configs.append(f"{key}={value}")
255
+
256
+ if configs:
257
+ args.extend(["--config", *configs])
258
+ if verbose:
259
+ typer.secho(f"snakemake {' '.join(args)}\n", fg=typer.colors.MAGENTA, err=True)
260
+ if not keep_snakemake and Path(".snakemake").exists():
261
+ keep_snakemake = True
262
+ try:
263
+ self.snk_config.add_resources(resource, self.workflow.path)
264
+ except FileNotFoundError as e:
265
+ self.error(str(e))
266
+ with self._copy_resources(
267
+ self.snk_config.resources,
268
+ cleanup=not keep_resources,
269
+ symlink_resources=self.snk_config.symlink_resources,
270
+ ):
271
+ if dag:
272
+ return self._save_dag(snakemake_args=args, filename=dag)
273
+ try:
274
+ snakemake.parse_config = parse_config_monkeypatch
275
+ snakemake.main(args)
276
+ except SystemExit as e:
277
+ status = int(str(e))
278
+ if status:
279
+ sys.exit(status)
280
+ if not keep_snakemake and Path(".snakemake").exists():
281
+ typer.secho("Cleaning up '.snakemake' folder... use --keep-snakemake to keep.", fg=typer.colors.YELLOW, err=True)
282
+ shutil.rmtree(".snakemake")
283
+
284
+ def _save_dag(self, snakemake_args: List[str], filename: Path):
285
+ from contextlib import redirect_stdout
286
+ import snakemake
287
+ import subprocess
288
+ import io
289
+
290
+ snakemake_args.append("--dag")
291
+
292
+ fileType = filename.suffix.lstrip(".")
293
+
294
+ # Create a file-like object to redirect the stdout
295
+ snakemake_output = io.StringIO()
296
+ # Use redirect_stdout to redirect stdout to the file-like object
297
+ with redirect_stdout(snakemake_output):
298
+ # Capture the output of snakemake.main(args) using a try-except block
299
+ try:
300
+ snakemake.parse_config = parse_config_monkeypatch
301
+ snakemake.main(snakemake_args)
302
+ except SystemExit: # Catch SystemExit exception to prevent termination
303
+ pass
304
+ try:
305
+ snakemake_output = snakemake_output.getvalue()
306
+ if "snakemake_dag" not in snakemake_output:
307
+ self.error("Could not generate dag!", exit=True)
308
+ # discard everything before digraph snakemake_dag
309
+ filtered_lines = (
310
+ "digraph snakemake_dag" + snakemake_output.split("snakemake_dag")[1]
311
+ )
312
+ echo_process = subprocess.Popen(
313
+ ["echo", filtered_lines], stdout=subprocess.PIPE
314
+ )
315
+ dot_process = subprocess.Popen(
316
+ ["dot", f"-T{fileType}"],
317
+ stdin=echo_process.stdout,
318
+ stdout=subprocess.PIPE,
319
+ )
320
+ with open(filename, "w") as output_file:
321
+ if self.verbose:
322
+ typer.secho(f"Saving dag to {filename}", fg=typer.colors.MAGENTA, err=True)
323
+ subprocess.run(["cat"], stdin=dot_process.stdout, stdout=output_file)
324
+ except (subprocess.CalledProcessError, FileNotFoundError):
325
+ typer.echo("dot command not found!", fg=typer.colors.RED, err=True)
326
+ raise typer.Exit(1)
327
+
328
+ @contextmanager
329
+ def _copy_resources(
330
+ self, resources: List[Path], cleanup: bool, symlink_resources: bool = False
331
+ ):
332
+ """
333
+ Copy resources to the current working directory.
334
+ Args:
335
+ resources (List[Path]): A list of paths to the resources to copy.
336
+ cleanup (bool): If True, the resources will be removed after the function exits.
337
+ Side Effects:
338
+ Copies the resources to the current working directory.
339
+ Returns:
340
+ Generator: A generator object.
341
+ Examples:
342
+ >>> with CLI.copy_resources(resources, cleanup=True):
343
+ ... # do something
344
+ """
345
+ import os
346
+ import shutil
347
+
348
+ copied_resources = []
349
+
350
+ def copy_resource(src: Path, dst: Path, symlink: bool = False):
351
+ if self.verbose:
352
+ typer.secho(
353
+ f" - Copying resource '{src}' to '{dst}'",
354
+ fg=typer.colors.MAGENTA,
355
+ err=True,
356
+ )
357
+ target_is_directory = src.is_dir()
358
+ if symlink:
359
+ os.symlink(src, dst, target_is_directory=target_is_directory)
360
+ elif target_is_directory:
361
+ shutil.copytree(src, dst)
362
+ else:
363
+ shutil.copy(src, dst)
364
+
365
+ def remove_resource(resource: Path):
366
+ if resource.is_symlink():
367
+ resource.unlink()
368
+ elif resource.is_dir():
369
+ shutil.rmtree(resource)
370
+ else:
371
+ os.remove(resource)
372
+
373
+ resources_folder = self.workflow.path / "resources"
374
+ if resources_folder.exists():
375
+ resources.insert(0, Path("resources"))
376
+ if self.verbose and resources:
377
+ typer.secho(
378
+ f"Copying {len(resources)} resources to working directory...",
379
+ fg=typer.colors.MAGENTA,
380
+ err=True,
381
+ )
382
+ try:
383
+ for resource in resources:
384
+ abs_path = self.workflow.path / resource
385
+ destination = Path(".") / resource.name
386
+ if not destination.exists():
387
+ # make sure you don't delete files that are already there...
388
+ copy_resource(abs_path, destination, symlink=symlink_resources)
389
+ copied_resources.append(destination)
390
+ elif self.verbose:
391
+ typer.secho(
392
+ f" - Resource '{resource.name}' already exists! Skipping...",
393
+ fg=typer.colors.MAGENTA,
394
+ err=True,
395
+ )
396
+ yield
397
+ finally:
398
+ if not cleanup:
399
+ return
400
+ for copied_resource in copied_resources:
401
+ if copied_resource.exists():
402
+ if self.verbose:
403
+ typer.secho(
404
+ f"Deleting '{copied_resource.name}' resource...",
405
+ fg=typer.colors.MAGENTA,
406
+ err=True,
407
+ )
408
+ remove_resource(copied_resource)
409
+
410
+
411
+ def parse_config_monkeypatch(args):
412
+ """Monkeypatch the parse_config function from snakemake."""
413
+ import yaml
414
+ import snakemake
415
+ import re
416
+
417
+ class NoDatesSafeLoader(yaml.SafeLoader):
418
+ @classmethod
419
+ def remove_implicit_resolver(cls, tag_to_remove):
420
+ """
421
+ Remove implicit resolvers for a particular tag
422
+
423
+ Takes care not to modify resolvers in super classes.
424
+
425
+ We want to load datetimes as strings, not dates, because we
426
+ go on to serialise as json which doesn't have the advanced types
427
+ of yaml, and leads to incompatibilities down the track.
428
+ """
429
+ if "yaml_implicit_resolvers" not in cls.__dict__:
430
+ cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy()
431
+
432
+ for first_letter, mappings in cls.yaml_implicit_resolvers.items():
433
+ cls.yaml_implicit_resolvers[first_letter] = [
434
+ (tag, regexp) for tag, regexp in mappings if tag != tag_to_remove
435
+ ]
436
+
437
+ NoDatesSafeLoader.remove_implicit_resolver("tag:yaml.org,2002:timestamp")
438
+
439
+ def _yaml_safe_load(s):
440
+ """Load yaml string safely."""
441
+ s = s.replace(": None", ": null")
442
+ return yaml.load(s, Loader=NoDatesSafeLoader)
443
+
444
+ parsers = [int, float, snakemake._bool_parser, _yaml_safe_load, str]
445
+ config = dict()
446
+ if args.config is not None:
447
+ valid = re.compile(r"[a-zA-Z_]\w*$")
448
+ for entry in args.config:
449
+ key, val = snakemake.parse_key_value_arg(
450
+ entry,
451
+ errmsg="Invalid config definition: Config entries have to be defined as name=value pairs.",
452
+ )
453
+ if not valid.match(key):
454
+ raise ValueError(
455
+ "Invalid config definition: Config entry must start with a valid identifier."
456
+ )
457
+ v = None
458
+ if val == "" or val == "None":
459
+ snakemake.update_config(config, {key: v})
460
+ continue
461
+ for parser in parsers:
462
+ try:
463
+ v = parser(val)
464
+ # avoid accidental interpretation as function
465
+ if not callable(v):
466
+ break
467
+ except:
468
+ pass
469
+ assert v is not None
470
+ snakemake.update_config(config, {key: v})
471
+ return config
@@ -0,0 +1,134 @@
1
+ import os
2
+ import subprocess
3
+ from pathlib import Path
4
+ import sys
5
+ from typing import List
6
+ import typer
7
+
8
+ from ..dynamic_typer import DynamicTyper
9
+ from .utils import create_snakemake_workflow
10
+ from ..workflow import Workflow
11
+ from rich.console import Console
12
+ from rich.syntax import Syntax
13
+ from snakemake.deployment.conda import Conda, Env
14
+ from snk_cli.config.config import get_config_from_workflow_dir
15
+
16
+
17
+ class ScriptApp(DynamicTyper):
18
+ def __init__(
19
+ self,
20
+ workflow: Workflow,
21
+ conda_prefix_dir: Path,
22
+ snakemake_config,
23
+ snakefile: Path,
24
+ ):
25
+ self.workflow = workflow
26
+ self.conda_prefix_dir = conda_prefix_dir
27
+ self.snakemake_config = snakemake_config
28
+ self.snakefile = snakefile
29
+ self.configfile = get_config_from_workflow_dir(self.workflow.path)
30
+ self.register_command(self.list, help="List the scripts in the workflow.")
31
+ self.register_command(
32
+ self.show, help="Show the contents of a script."
33
+ )
34
+ self.register_command(
35
+ self.run,
36
+ help=f"""
37
+ Run a script from the workflow.
38
+ \n\nThe executor for the script is inferred from the suffix.
39
+ \n\nExample: {self.workflow.name} script run --env=python script.py
40
+ \n\nTo pass help to the script, use `-- --help`.""",
41
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
42
+ )
43
+
44
+ def list(
45
+ self,
46
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show script as paths."),
47
+ ):
48
+ from rich.console import Console
49
+ from rich.table import Table
50
+ table = Table("Name", "CMD", "File", show_header=True, show_lines=True)
51
+ for script in self.workflow.scripts:
52
+ # address relative to cwd
53
+ table.add_row(script.stem, f"{self.workflow.name} script show {script.stem}", str(script.resolve()) if verbose else script.name)
54
+ console = Console()
55
+ console.print(table)
56
+
57
+ def _get_script_path(self, name: str) -> Path:
58
+ script = [e for e in self.workflow.scripts if e.name == name or e.stem == name]
59
+ if not script:
60
+ self.error(f"Script {name} not found!")
61
+ return script[0]
62
+
63
+ def _get_conda_env_path(self, name: str) -> Path:
64
+ env = [e for e in self.workflow.environments if e.stem == name]
65
+ if not env:
66
+ self.error(f"Environment {name} not found!")
67
+ return env[0]
68
+
69
+ def _shellcmd(self, env_address: str, cmd: str) -> str:
70
+ if sys.platform.lower().startswith("win"):
71
+ return Conda().shellcmd_win(env_address, cmd)
72
+ return Conda().shellcmd(env_address, cmd)
73
+
74
+ def show(
75
+ self,
76
+ name: str = typer.Argument(..., help="The name of the script."),
77
+ pretty: bool = typer.Option(
78
+ False, "--pretty", "-p", help="Pretty print the script."
79
+ ),
80
+ ):
81
+ script_path = self._get_script_path(name)
82
+ code = script_path.read_text()
83
+ if pretty:
84
+ code = Syntax(code, script_path.suffix[1:])
85
+ console = Console()
86
+ console.print(code)
87
+ else:
88
+ typer.echo(code)
89
+
90
+ def _get_executor(self, suffix: str) -> str:
91
+ if suffix == "py":
92
+ return "python"
93
+ elif suffix == "R":
94
+ return "Rscript"
95
+ elif suffix == "sh":
96
+ return "bash"
97
+ elif suffix == "pl":
98
+ return "perl"
99
+ elif suffix == "Rmd" or suffix == "rmd" or suffix == "Rhtml":
100
+ return "Rscript -e 'rmarkdown::render(\"{script_path}\")'"
101
+ elif suffix == "ipynb":
102
+ return "jupyter nbconvert --to notebook --execute --inplace --ExecutePreprocessor.timeout=-1 {script_path}"
103
+ elif suffix == "Rnw":
104
+ return "Rscript -e 'knitr::knit(\"{script_path}\")'"
105
+ else:
106
+ self.error(f"Unknown script suffix: {suffix}!")
107
+
108
+ def run(
109
+ self,
110
+ env: str = typer.Option(None, help="The name of the environment to run script in."),
111
+ name: str = typer.Argument(..., help="The name of the script."),
112
+ args: List[str] = typer.Argument(None, help="Arguments to pass to the script."),
113
+ ):
114
+ script_path = self._get_script_path(name)
115
+ executor = self._get_executor(script_path.suffix[1:])
116
+ cmd = [executor, f'"{script_path}"'] + args
117
+ if env:
118
+ env_path = self._get_conda_env_path(env)
119
+ workflow = create_snakemake_workflow(
120
+ self.snakefile,
121
+ config=self.snakemake_config,
122
+ configfiles=[self.configfile] if self.configfile else None,
123
+ use_conda=True,
124
+ conda_prefix=self.conda_prefix_dir.resolve(),
125
+ )
126
+ env = Env(workflow, env_file=env_path.resolve())
127
+ env.create()
128
+ cmd = self._shellcmd(env.address, " ".join(cmd))
129
+ else:
130
+ cmd = " ".join(cmd)
131
+ user_shell = os.environ.get("SHELL", "/bin/bash")
132
+ subprocess.run(cmd, shell=True, env=os.environ.copy(), executable=user_shell)
133
+
134
+