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.
- snk_cli/__about__.py +4 -0
- snk_cli/__init__.py +5 -0
- snk_cli/cli.py +231 -0
- snk_cli/config/__init__.py +1 -0
- snk_cli/config/config.py +219 -0
- snk_cli/config/utils.py +41 -0
- snk_cli/dynamic_typer.py +249 -0
- snk_cli/options/__init__.py +1 -0
- snk_cli/options/option.py +18 -0
- snk_cli/options/utils.py +117 -0
- snk_cli/subcommands/__init__.py +5 -0
- snk_cli/subcommands/config.py +64 -0
- snk_cli/subcommands/env.py +211 -0
- snk_cli/subcommands/profile.py +60 -0
- snk_cli/subcommands/run.py +471 -0
- snk_cli/subcommands/script.py +134 -0
- snk_cli/subcommands/utils.py +175 -0
- snk_cli/testing.py +19 -0
- snk_cli/utils.py +149 -0
- snk_cli/validate.py +66 -0
- snk_cli/workflow.py +177 -0
- snk_cli-0.0.1.dist-info/METADATA +29 -0
- snk_cli-0.0.1.dist-info/RECORD +25 -0
- snk_cli-0.0.1.dist-info/WHEEL +4 -0
- snk_cli-0.0.1.dist-info/licenses/LICENSE.txt +9 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from snakemake import (
|
|
4
|
+
Workflow,
|
|
5
|
+
update_config,
|
|
6
|
+
load_configfile,
|
|
7
|
+
dict_to_key_value_args,
|
|
8
|
+
common,
|
|
9
|
+
)
|
|
10
|
+
from snakemake.persistence import Persistence
|
|
11
|
+
from ..utils import check_command_available
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PersistenceMock(Persistence):
|
|
16
|
+
"""
|
|
17
|
+
Mock for workflow.persistence
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
conda_env_path: Path = None
|
|
21
|
+
_metadata_path: Path = None
|
|
22
|
+
_incomplete_path: Path = None
|
|
23
|
+
shadow_path: Path = None
|
|
24
|
+
conda_env_archive_path: Path = None
|
|
25
|
+
container_img_path: Path = None
|
|
26
|
+
aux_path: Path = None
|
|
27
|
+
|
|
28
|
+
def create_snakemake_workflow(
|
|
29
|
+
snakefile,
|
|
30
|
+
cache=None,
|
|
31
|
+
lint=None,
|
|
32
|
+
cores=1,
|
|
33
|
+
nodes=None,
|
|
34
|
+
max_threads=None,
|
|
35
|
+
resources=dict(),
|
|
36
|
+
overwrite_threads=None,
|
|
37
|
+
overwrite_scatter=None,
|
|
38
|
+
overwrite_resource_scopes=None,
|
|
39
|
+
default_resources=None,
|
|
40
|
+
overwrite_resources=None,
|
|
41
|
+
config=dict(),
|
|
42
|
+
configfiles=None,
|
|
43
|
+
config_args=None,
|
|
44
|
+
workdir=None,
|
|
45
|
+
printshellcmds=False,
|
|
46
|
+
rerun_triggers=["mtime", "params", "input", "software-env", "code"],
|
|
47
|
+
conda_cleanup_envs=False,
|
|
48
|
+
latency_wait=3,
|
|
49
|
+
print_compilation=False,
|
|
50
|
+
debug=False,
|
|
51
|
+
all_temp=False,
|
|
52
|
+
jobscript=None,
|
|
53
|
+
overwrite_shellcmd=None,
|
|
54
|
+
restart_times=0,
|
|
55
|
+
attempt=1,
|
|
56
|
+
verbose=False,
|
|
57
|
+
use_conda=False,
|
|
58
|
+
use_singularity=False,
|
|
59
|
+
use_env_modules=False,
|
|
60
|
+
singularity_args="",
|
|
61
|
+
conda_frontend="conda",
|
|
62
|
+
conda_prefix=None,
|
|
63
|
+
conda_cleanup_pkgs=None,
|
|
64
|
+
list_conda_envs=False,
|
|
65
|
+
singularity_prefix=None,
|
|
66
|
+
shadow_prefix=None,
|
|
67
|
+
scheduler="ilp",
|
|
68
|
+
scheduler_ilp_solver=None,
|
|
69
|
+
mode=common.Mode.default,
|
|
70
|
+
wrapper_prefix=None,
|
|
71
|
+
default_remote_prefix="",
|
|
72
|
+
assume_shared_fs=True,
|
|
73
|
+
keep_metadata=True,
|
|
74
|
+
edit_notebook=None,
|
|
75
|
+
envvars=None,
|
|
76
|
+
overwrite_groups=None,
|
|
77
|
+
group_components=None,
|
|
78
|
+
max_inventory_wait_time=20,
|
|
79
|
+
execute_subworkflows=True,
|
|
80
|
+
conda_not_block_search_path_envvars=False,
|
|
81
|
+
scheduler_solver_path=None,
|
|
82
|
+
conda_base_path=None,
|
|
83
|
+
local_groupid="local",
|
|
84
|
+
):
|
|
85
|
+
overwrite_config = dict()
|
|
86
|
+
if configfiles is None:
|
|
87
|
+
configfiles = []
|
|
88
|
+
for f in configfiles:
|
|
89
|
+
# get values to override. Later configfiles override earlier ones.
|
|
90
|
+
update_config(overwrite_config, load_configfile(f))
|
|
91
|
+
# convert provided paths to absolute paths
|
|
92
|
+
configfiles = list(map(os.path.abspath, configfiles))
|
|
93
|
+
|
|
94
|
+
# directly specified elements override any configfiles
|
|
95
|
+
if config:
|
|
96
|
+
update_config(overwrite_config, config)
|
|
97
|
+
if config_args is None:
|
|
98
|
+
config_args = dict_to_key_value_args(config)
|
|
99
|
+
|
|
100
|
+
if workdir:
|
|
101
|
+
if not os.path.exists(workdir):
|
|
102
|
+
os.makedirs(workdir)
|
|
103
|
+
workdir = os.path.abspath(workdir)
|
|
104
|
+
os.chdir(workdir)
|
|
105
|
+
|
|
106
|
+
if check_command_available("mamba"):
|
|
107
|
+
conda_frontend = "mamba"
|
|
108
|
+
|
|
109
|
+
workflow = Workflow(
|
|
110
|
+
snakefile=snakefile,
|
|
111
|
+
rerun_triggers=rerun_triggers,
|
|
112
|
+
jobscript=jobscript,
|
|
113
|
+
overwrite_shellcmd=overwrite_shellcmd,
|
|
114
|
+
overwrite_config=overwrite_config,
|
|
115
|
+
overwrite_workdir=workdir,
|
|
116
|
+
overwrite_configfiles=configfiles,
|
|
117
|
+
overwrite_clusterconfig=dict(),
|
|
118
|
+
overwrite_threads=overwrite_threads,
|
|
119
|
+
max_threads=max_threads,
|
|
120
|
+
overwrite_scatter=overwrite_scatter,
|
|
121
|
+
overwrite_groups=overwrite_groups,
|
|
122
|
+
overwrite_resources=overwrite_resources,
|
|
123
|
+
overwrite_resource_scopes=overwrite_resource_scopes,
|
|
124
|
+
group_components=group_components,
|
|
125
|
+
config_args=config_args,
|
|
126
|
+
debug=debug,
|
|
127
|
+
verbose=verbose,
|
|
128
|
+
use_conda=use_conda or list_conda_envs or conda_cleanup_envs,
|
|
129
|
+
use_singularity=use_singularity,
|
|
130
|
+
use_env_modules=use_env_modules,
|
|
131
|
+
conda_frontend=conda_frontend,
|
|
132
|
+
conda_prefix=conda_prefix,
|
|
133
|
+
conda_cleanup_pkgs=conda_cleanup_pkgs,
|
|
134
|
+
singularity_prefix=singularity_prefix,
|
|
135
|
+
shadow_prefix=shadow_prefix,
|
|
136
|
+
singularity_args=singularity_args,
|
|
137
|
+
scheduler_type=scheduler,
|
|
138
|
+
scheduler_ilp_solver=scheduler_ilp_solver,
|
|
139
|
+
mode=mode,
|
|
140
|
+
wrapper_prefix=wrapper_prefix,
|
|
141
|
+
printshellcmds=printshellcmds,
|
|
142
|
+
restart_times=restart_times,
|
|
143
|
+
attempt=attempt,
|
|
144
|
+
default_remote_provider=None,
|
|
145
|
+
default_remote_prefix=default_remote_prefix,
|
|
146
|
+
run_local=True,
|
|
147
|
+
assume_shared_fs=assume_shared_fs,
|
|
148
|
+
default_resources=default_resources,
|
|
149
|
+
cache=cache,
|
|
150
|
+
cores=cores,
|
|
151
|
+
nodes=nodes,
|
|
152
|
+
resources=resources,
|
|
153
|
+
edit_notebook=edit_notebook,
|
|
154
|
+
envvars=envvars,
|
|
155
|
+
max_inventory_wait_time=max_inventory_wait_time,
|
|
156
|
+
conda_not_block_search_path_envvars=conda_not_block_search_path_envvars,
|
|
157
|
+
execute_subworkflows=execute_subworkflows,
|
|
158
|
+
scheduler_solver_path=scheduler_solver_path,
|
|
159
|
+
conda_base_path=conda_base_path,
|
|
160
|
+
check_envvars=not lint, # for linting, we do not need to check whether requested envvars exist
|
|
161
|
+
all_temp=all_temp,
|
|
162
|
+
local_groupid=local_groupid,
|
|
163
|
+
keep_metadata=keep_metadata,
|
|
164
|
+
latency_wait=latency_wait,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
persistence = PersistenceMock(
|
|
168
|
+
conda_env_path=Path(conda_prefix).resolve() if conda_prefix else None,
|
|
169
|
+
conda_env_archive_path=os.path.join(Path(".snakemake"), "conda-archive"),
|
|
170
|
+
)
|
|
171
|
+
if hasattr(workflow, "_persistence"):
|
|
172
|
+
workflow._persistence = persistence
|
|
173
|
+
else:
|
|
174
|
+
workflow.persistence = persistence
|
|
175
|
+
return workflow
|
snk_cli/testing.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typer.testing import CliRunner, Result
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List
|
|
4
|
+
from snk_cli import CLI
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SnkCliRunner:
|
|
9
|
+
"""Dynamically create a CLI Runner for testing"""
|
|
10
|
+
|
|
11
|
+
cli: CLI
|
|
12
|
+
runner = CliRunner(mix_stderr=False)
|
|
13
|
+
|
|
14
|
+
def invoke(self, args: List[str]) -> Result:
|
|
15
|
+
old_argv = sys.argv
|
|
16
|
+
sys.argv = ['cli'] + args # ensure that the CLI is invoked with the correct arguments
|
|
17
|
+
result = self.runner.invoke(self.cli.app, args)
|
|
18
|
+
sys.argv = old_argv
|
|
19
|
+
return result
|
snk_cli/utils.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import typer
|
|
5
|
+
import sys
|
|
6
|
+
import collections # MutableMapping import hack
|
|
7
|
+
|
|
8
|
+
if sys.version_info.major == 3 and sys.version_info.minor >= 10:
|
|
9
|
+
from collections.abc import MutableMapping
|
|
10
|
+
else:
|
|
11
|
+
from collections import MutableMapping
|
|
12
|
+
from snk_cli.options import Option
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_command_available(command: str):
|
|
16
|
+
"""
|
|
17
|
+
Check if a command is available.
|
|
18
|
+
Args:
|
|
19
|
+
command (str): The command to check.
|
|
20
|
+
Returns:
|
|
21
|
+
bool: True if the command is available, False otherwise.
|
|
22
|
+
Examples:
|
|
23
|
+
>>> CLI.check_command_available('ls')
|
|
24
|
+
"""
|
|
25
|
+
from shutil import which
|
|
26
|
+
|
|
27
|
+
return which(command) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def flatten(d, parent_key="", sep=":"):
|
|
31
|
+
"""
|
|
32
|
+
Flattens a nested dictionary.
|
|
33
|
+
Args:
|
|
34
|
+
d (dict): The dictionary to flatten.
|
|
35
|
+
parent_key (str, optional): The parent key of the dictionary. Defaults to ''.
|
|
36
|
+
sep (str, optional): The separator to use between keys. Defaults to ':'.
|
|
37
|
+
Returns:
|
|
38
|
+
dict: A flattened dictionary.
|
|
39
|
+
Examples:
|
|
40
|
+
>>> d = {'a': {'b': 1, 'c': 2}, 'd': 3}
|
|
41
|
+
>>> flatten(d)
|
|
42
|
+
{'a:b': 1, 'a:c': 2, 'd': 3}
|
|
43
|
+
"""
|
|
44
|
+
items = []
|
|
45
|
+
for k, v in d.items():
|
|
46
|
+
new_key = parent_key + sep + k if parent_key else k
|
|
47
|
+
if isinstance(v, MutableMapping):
|
|
48
|
+
items.extend(flatten(v, new_key, sep=sep).items())
|
|
49
|
+
else:
|
|
50
|
+
items.append((new_key, v))
|
|
51
|
+
return dict(items)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def convert_key_to_snakemake_format(key, value, sep=":"):
|
|
55
|
+
"""
|
|
56
|
+
Convert key to a format that can be passed over the cli to snakemake
|
|
57
|
+
"""
|
|
58
|
+
result_dict = {}
|
|
59
|
+
parts = key.split(sep)
|
|
60
|
+
current_dict = result_dict
|
|
61
|
+
|
|
62
|
+
for part in parts[:-1]:
|
|
63
|
+
current_dict = current_dict.setdefault(part, {})
|
|
64
|
+
|
|
65
|
+
current_dict[parts[-1]] = value
|
|
66
|
+
|
|
67
|
+
return result_dict
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def serialise(d):
|
|
71
|
+
"""
|
|
72
|
+
Serialises a data structure into a string.
|
|
73
|
+
Args:
|
|
74
|
+
d (any): The data structure to serialise.
|
|
75
|
+
Returns:
|
|
76
|
+
any: The serialised data structure.
|
|
77
|
+
Examples:
|
|
78
|
+
>>> serialise({'a': 1, 'b': 2})
|
|
79
|
+
{'a': '1', 'b': '2'}
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(d, Path) or isinstance(d, datetime):
|
|
82
|
+
return str(d)
|
|
83
|
+
|
|
84
|
+
if isinstance(d, list):
|
|
85
|
+
return [serialise(x) for x in d]
|
|
86
|
+
|
|
87
|
+
if isinstance(d, dict):
|
|
88
|
+
for k, v in d.items():
|
|
89
|
+
d.update({k: serialise(v)})
|
|
90
|
+
|
|
91
|
+
# return anything else, like a string or number
|
|
92
|
+
return d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_config_args(args: List[str], options: List[Option]):
|
|
96
|
+
"""
|
|
97
|
+
Parses a list of arguments and a list of options.
|
|
98
|
+
Args:
|
|
99
|
+
args (List[str]): A list of arguments.
|
|
100
|
+
options (List[Option]): A list of options.
|
|
101
|
+
Returns:
|
|
102
|
+
(List[str], List[dict]): A tuple of parsed arguments and config.
|
|
103
|
+
Examples:
|
|
104
|
+
>>> parse_config_args(['-name', 'John', '-age', '20'], [{'name': 'name', 'default': '', 'help': '', 'type': 'str', 'required': True}, {'name': 'age', 'default': '', 'help': '', 'type': 'int', 'required': True}])
|
|
105
|
+
(['John', '20'], [{'name': 'name', 'John'}, {'age': 20}])
|
|
106
|
+
"""
|
|
107
|
+
names = [op.name for op in options]
|
|
108
|
+
config = []
|
|
109
|
+
parsed: List[str] = []
|
|
110
|
+
flag = None
|
|
111
|
+
for arg in args:
|
|
112
|
+
if flag:
|
|
113
|
+
name = flag.lstrip("-")
|
|
114
|
+
op = next(op for op in options if op.name == name)
|
|
115
|
+
if op.updated is False and op.default == serialise(arg):
|
|
116
|
+
# skip args that don't change
|
|
117
|
+
flag = None
|
|
118
|
+
continue
|
|
119
|
+
if ":" in op.original_key:
|
|
120
|
+
samkemake_format_config = convert_key_to_snakemake_format(
|
|
121
|
+
op.original_key, arg
|
|
122
|
+
)
|
|
123
|
+
name = list(samkemake_format_config.keys())[0]
|
|
124
|
+
arg = samkemake_format_config[name]
|
|
125
|
+
config.append({name: serialise(arg)})
|
|
126
|
+
flag = None
|
|
127
|
+
continue
|
|
128
|
+
if arg.startswith("-") and arg.lstrip("-") in names:
|
|
129
|
+
flag = arg
|
|
130
|
+
continue
|
|
131
|
+
parsed.append(arg)
|
|
132
|
+
parsed.sort()
|
|
133
|
+
return parsed, config
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_default_type(v):
|
|
137
|
+
default_type = type(v)
|
|
138
|
+
if default_type == list and len(v) > 0:
|
|
139
|
+
return f"List[{type(v[0]).__name__}]"
|
|
140
|
+
return str(default_type.__name__)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def dag_filetype_callback(ctx: typer.Context, file: Path):
|
|
144
|
+
allowed = [".pdf", ".png", ".svg"]
|
|
145
|
+
if ctx.resilient_parsing or not file:
|
|
146
|
+
return
|
|
147
|
+
if file.suffix not in allowed:
|
|
148
|
+
raise typer.BadParameter(f"Dag file suffix must be one of {','.join(allowed)}!")
|
|
149
|
+
return file
|
snk_cli/validate.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, Union
|
|
3
|
+
from .config import SnkConfig
|
|
4
|
+
from .options.utils import types
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
class ValidationError(Exception):
|
|
8
|
+
"""Base class for all validation exceptions"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_config(config: Dict[str, Any], snk_config_path: Path) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Validates the config against the snk config.
|
|
14
|
+
Will convert values to the correct type if possible.
|
|
15
|
+
Args:
|
|
16
|
+
config (dict): The config to validate.
|
|
17
|
+
snk_config_path (Path): The path to the snk config.
|
|
18
|
+
"""
|
|
19
|
+
snk_config_path = Path(snk_config_path)
|
|
20
|
+
# if relative path, make absolute to
|
|
21
|
+
if not snk_config_path.is_absolute():
|
|
22
|
+
frame = inspect.currentframe().f_back
|
|
23
|
+
workflow = frame.f_globals.get("workflow")
|
|
24
|
+
snk_config_path = Path(workflow.current_basedir) / snk_config_path
|
|
25
|
+
|
|
26
|
+
snk_config = SnkConfig.from_path(snk_config_path)
|
|
27
|
+
validate_and_transform_in_place(config, snk_config.cli)
|
|
28
|
+
|
|
29
|
+
ValidationDict = Dict[str, Union["ValidationDict", Dict[str, str]]]
|
|
30
|
+
|
|
31
|
+
def validate_and_transform_in_place(config: Dict[str, Any], validation: ValidationDict, replace_none: bool = True) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Validates the config against the snk config.
|
|
34
|
+
Will convert values to the correct type if possible.
|
|
35
|
+
Args:
|
|
36
|
+
config (dict): The config to validate.
|
|
37
|
+
validation (dict): The validation dict.
|
|
38
|
+
replace_none (bool): If True, replace 'None' with None.
|
|
39
|
+
"""
|
|
40
|
+
for key, value in list(config.items()):
|
|
41
|
+
if key not in validation:
|
|
42
|
+
continue # Optionally handle unexpected keys
|
|
43
|
+
if value == 'None' and replace_none:
|
|
44
|
+
config[key] = None
|
|
45
|
+
continue
|
|
46
|
+
if value is None:
|
|
47
|
+
continue
|
|
48
|
+
val_info = validation[key]
|
|
49
|
+
if isinstance(val_info, dict) and 'type' in val_info:
|
|
50
|
+
# Direct type validation
|
|
51
|
+
val_type = types.get(val_info["type"], None)
|
|
52
|
+
if val_type is None:
|
|
53
|
+
raise ValueError(f"Unknown type '{val_info['type']}'")
|
|
54
|
+
try:
|
|
55
|
+
if getattr(val_type, "__origin__", None) == list:
|
|
56
|
+
val_type = val_type.__args__[0]
|
|
57
|
+
if not isinstance(value, list):
|
|
58
|
+
raise ValueError(f"Expected a list for key '{key}'")
|
|
59
|
+
config[key] = [val_type(v) for v in value]
|
|
60
|
+
else:
|
|
61
|
+
config[key] = val_type(value)
|
|
62
|
+
except (ValueError, TypeError) as e:
|
|
63
|
+
raise ValueError(f"Type conversion error for key '{key}': {e}")
|
|
64
|
+
elif isinstance(value, dict):
|
|
65
|
+
# Nested dictionary validation
|
|
66
|
+
validate_and_transform_in_place(value, val_info)
|
snk_cli/workflow.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from git import Repo, InvalidGitRepositoryError
|
|
5
|
+
import importlib.util
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class Workflow:
|
|
9
|
+
"""
|
|
10
|
+
Represents a workflow.
|
|
11
|
+
Attributes:
|
|
12
|
+
path (Path): The path to the workflow.
|
|
13
|
+
repo (Repo): The git repository of the workflow.
|
|
14
|
+
name (str): The name of the workflow.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: Path) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initializes a Workflow object.
|
|
20
|
+
Args:
|
|
21
|
+
path (Path): The path to the workflow.
|
|
22
|
+
Returns:
|
|
23
|
+
None
|
|
24
|
+
Notes:
|
|
25
|
+
Initializes the `repo` and `name` attributes.
|
|
26
|
+
"""
|
|
27
|
+
self.path = path
|
|
28
|
+
self.editable = self.check_is_editable()
|
|
29
|
+
if self.editable: # editable mode
|
|
30
|
+
self.repo = None
|
|
31
|
+
else:
|
|
32
|
+
try:
|
|
33
|
+
self.repo = Repo(path)
|
|
34
|
+
except InvalidGitRepositoryError:
|
|
35
|
+
self.repo = None
|
|
36
|
+
self.name = self.path.name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def tag(self):
|
|
41
|
+
"""
|
|
42
|
+
Gets the tag of the workflow.
|
|
43
|
+
Returns:
|
|
44
|
+
str: The tag of the workflow, or None if no tag is found.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
tag = self.repo.git.describe(["--tags", "--exact-match"])
|
|
48
|
+
except Exception:
|
|
49
|
+
tag = None
|
|
50
|
+
return tag
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def commit(self):
|
|
54
|
+
"""
|
|
55
|
+
Gets the commit SHA of the workflow.
|
|
56
|
+
Returns:
|
|
57
|
+
str: The commit SHA of the workflow.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
sha = self.repo.head.object.hexsha
|
|
61
|
+
commit = self.repo.git.rev_parse(sha, short=8)
|
|
62
|
+
except Exception:
|
|
63
|
+
commit = None
|
|
64
|
+
return commit
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def version(self):
|
|
68
|
+
"""
|
|
69
|
+
Gets the version of the workflow.
|
|
70
|
+
Returns:
|
|
71
|
+
str: The version of the workflow, or None if no version is found.
|
|
72
|
+
"""
|
|
73
|
+
if self.repo is None:
|
|
74
|
+
return None
|
|
75
|
+
if self.tag:
|
|
76
|
+
version = self.tag
|
|
77
|
+
else:
|
|
78
|
+
version = self.commit
|
|
79
|
+
return version
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def executable(self):
|
|
83
|
+
"""
|
|
84
|
+
Gets the executable of the workflow.
|
|
85
|
+
Returns:
|
|
86
|
+
Path: The path to the workflow executable.
|
|
87
|
+
"""
|
|
88
|
+
workflow_bin_dir = self.path.parent.parent / "bin"
|
|
89
|
+
name = self.name
|
|
90
|
+
if sys.platform.startswith("win"):
|
|
91
|
+
name += ".exe"
|
|
92
|
+
return workflow_bin_dir / name
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def conda_prefix_dir(self):
|
|
96
|
+
"""
|
|
97
|
+
Gets the conda prefix directory of the workflow. If in editable mode, the conda prefix directory is
|
|
98
|
+
located in the .snakemake directory. Otherwise, it is located in the .conda directory in the workflow
|
|
99
|
+
directory.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Path: The path to the conda prefix directory.
|
|
103
|
+
"""
|
|
104
|
+
return Path(".snakemake") / "conda" if self.editable else self.path / ".conda"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def singularity_prefix_dir(self):
|
|
108
|
+
"""
|
|
109
|
+
Gets the singularity prefix directory of the workflow.
|
|
110
|
+
Returns:
|
|
111
|
+
Path: The path to the singularity prefix directory.
|
|
112
|
+
"""
|
|
113
|
+
if " " in str(self.path):
|
|
114
|
+
# sigh, snakemake singularity does not support spaces in the path
|
|
115
|
+
# https://github.com/snakemake/snakemake/blob/2ecb21ba04088b9e6850447760f713784cf8b775/snakemake/deployment/singularity.py#L130C1-L131C1
|
|
116
|
+
return None
|
|
117
|
+
return Path(".snakemake") / "singularity" if self.editable else self.path / ".singularity"
|
|
118
|
+
|
|
119
|
+
def _is_editable_pip_install(self):
|
|
120
|
+
# This function now acts as a method within the Workflow class
|
|
121
|
+
package_spec = importlib.util.find_spec(self.name)
|
|
122
|
+
if package_spec is None:
|
|
123
|
+
return False # Package is not installed
|
|
124
|
+
|
|
125
|
+
package_location = package_spec.origin
|
|
126
|
+
site_packages_paths = [p for p in sys.path if 'site-packages' in p]
|
|
127
|
+
is_inside_site_packages = any(package_location.startswith(sp) for sp in site_packages_paths)
|
|
128
|
+
|
|
129
|
+
if not is_inside_site_packages:
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
for sp in site_packages_paths:
|
|
133
|
+
egg_link_path = os.path.join(sp, self.name + '.egg-link')
|
|
134
|
+
if os.path.isfile(egg_link_path):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def check_is_editable(self):
|
|
140
|
+
"""Is the workflow editable?"""
|
|
141
|
+
if self.path.is_symlink():
|
|
142
|
+
return True
|
|
143
|
+
try:
|
|
144
|
+
return self._is_editable_pip_install()
|
|
145
|
+
except Exception:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def _find_folder(self, name) -> Optional[Path]:
|
|
149
|
+
"""Search for folder"""
|
|
150
|
+
if (self.path / "workflow" / name).exists():
|
|
151
|
+
return self.path / "workflow" / name
|
|
152
|
+
if (self.path / name).exists():
|
|
153
|
+
return self.path / name
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def profiles(self):
|
|
158
|
+
workflow_profile_dir = self._find_folder("profiles")
|
|
159
|
+
if workflow_profile_dir:
|
|
160
|
+
return [p for p in workflow_profile_dir.glob("*") if p.is_dir() and (p / "config.yaml").exists()]
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def environments(self):
|
|
165
|
+
workflow_environments_dir = self._find_folder("envs")
|
|
166
|
+
if workflow_environments_dir:
|
|
167
|
+
return [e for e in workflow_environments_dir.glob("*.yaml")] + [
|
|
168
|
+
e for e in workflow_environments_dir.glob("*.yml")
|
|
169
|
+
]
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def scripts(self):
|
|
174
|
+
workflow_environments_dir = self._find_folder("scripts")
|
|
175
|
+
if workflow_environments_dir:
|
|
176
|
+
return [s for s in workflow_environments_dir.iterdir() if s.is_file()]
|
|
177
|
+
return []
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: snk-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Project-URL: Documentation, https://github.com/unknown/snk-cli#readme
|
|
5
|
+
Project-URL: Issues, https://github.com/unknown/snk-cli/issues
|
|
6
|
+
Project-URL: Source, https://github.com/unknown/snk-cli
|
|
7
|
+
Author-email: Wytamma Wirth <wytamma.wirth@me.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Requires-Dist: art~=5.9
|
|
21
|
+
Requires-Dist: datrie>=0.8.2
|
|
22
|
+
Requires-Dist: gitpython~=3.1
|
|
23
|
+
Requires-Dist: makefun~=1.15
|
|
24
|
+
Requires-Dist: pulp<2.8
|
|
25
|
+
Requires-Dist: snakemake<9,>=7
|
|
26
|
+
Requires-Dist: typer[all]~=0.9.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# snk-cli
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
snk_cli/__about__.py,sha256=I1WJyeCzQRHJXTtCdvlSm6jF2FfYm8nKcOMZELFuzCE,131
|
|
2
|
+
snk_cli/__init__.py,sha256=iZGwZ9O_cUMK1OC2v6HxIHeLf-TQ_aKxPbVrtdRU0gI,167
|
|
3
|
+
snk_cli/cli.py,sha256=K6hs5PpThGDPtXYrYxgRcUhwm1MEKcQSKH4rra-Nq1Y,7807
|
|
4
|
+
snk_cli/dynamic_typer.py,sha256=h743N18SYtYXivhDM6yp8fenmNFHKWa5zTGgazPBk40,9137
|
|
5
|
+
snk_cli/testing.py,sha256=F8kElQOM73XAfo5OVmpNU0KILPp88059PTwJkDt9aWc,556
|
|
6
|
+
snk_cli/utils.py,sha256=aFUZjbKNfUtqtPL1SPQ4qae2FkDJQ36QRUA0YRnqfpM,4482
|
|
7
|
+
snk_cli/validate.py,sha256=4VKstHpDmM2AuyPjkvQXenClMskOjZ5dJyW1FPuiqrU,2686
|
|
8
|
+
snk_cli/workflow.py,sha256=fMyZ1nXtDMoLf3Aa3csOC5E1IokV8EcSQp84hSajnIE,5626
|
|
9
|
+
snk_cli/config/__init__.py,sha256=pbFmOEXCm6dG0eIsAoozMmXN9VomL5uuO29oHTmu3Ew,30
|
|
10
|
+
snk_cli/config/config.py,sha256=eK4qh0jSS-hU1CYqFR3JD780Zju3nngOs-AGgjWNf2E,8309
|
|
11
|
+
snk_cli/config/utils.py,sha256=7nhOas6Q1WV7EEdlKbVRkbTprx0SMCzDJ6kbx-vKmZc,1378
|
|
12
|
+
snk_cli/options/__init__.py,sha256=Vz2wNDyY_AacPrU439TteFvEXvBEdmb0dDYlvJN_LzA,41
|
|
13
|
+
snk_cli/options/option.py,sha256=4IzW6l0fMN-ilagkGVmivNm7dkEBvfu5iVuO7WuOx7Y,340
|
|
14
|
+
snk_cli/options/utils.py,sha256=6ofFLX6UbIj0RqwQxfN-sL7oShy2a4RMXEQZrDSoGWk,3838
|
|
15
|
+
snk_cli/subcommands/__init__.py,sha256=Q0kkzHRumlpGQdLSYGcGKZJRtQXYDVx-F_4IvP-274s,140
|
|
16
|
+
snk_cli/subcommands/config.py,sha256=wv-m-gAgK1BnKCeUBLf3XAh_Xmyz7oJMTlafdhpL3NY,2268
|
|
17
|
+
snk_cli/subcommands/env.py,sha256=XHrWbb0IHHA6e7oEyFjt_M-pPovTCDV9ZpvpKN7R_98,8051
|
|
18
|
+
snk_cli/subcommands/profile.py,sha256=4xY6DV7H_Cylx9wsyBtRB7LpNrqydEa-FiU0hS4bodw,2032
|
|
19
|
+
snk_cli/subcommands/run.py,sha256=bgwGbVqpMuw1rsssz051OPHX4FyjYxXbJU-fTq5i6Og,17139
|
|
20
|
+
snk_cli/subcommands/script.py,sha256=2cHbFwpdUhLFC7naPbUxtmdBYYGYUx8-GGF-TZ-jl80,5126
|
|
21
|
+
snk_cli/subcommands/utils.py,sha256=z2UK40oqzDClzhajtTBoRiUmp2A_ztDm3wueIpL0BdA,5470
|
|
22
|
+
snk_cli-0.0.1.dist-info/METADATA,sha256=JEV1XOPqrtkaV9q0scaHhzbkhyUsxtORi6bDWvn_CRY,1095
|
|
23
|
+
snk_cli-0.0.1.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
24
|
+
snk_cli-0.0.1.dist-info/licenses/LICENSE.txt,sha256=2EtbIEsC6oT0hDoWB8mP643X4l51wwQRdhofPSN0MLc,1101
|
|
25
|
+
snk_cli-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Wytamma Wirth <wytamma.wirth@me.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|