snk-cli 0.0.1__tar.gz

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.
Files changed (56) hide show
  1. snk_cli-0.0.1/.github/workflows/publish.yml +28 -0
  2. snk_cli-0.0.1/.github/workflows/tests.yml +38 -0
  3. snk_cli-0.0.1/.gitignore +13 -0
  4. snk_cli-0.0.1/LICENSE.txt +9 -0
  5. snk_cli-0.0.1/PKG-INFO +29 -0
  6. snk_cli-0.0.1/README.md +1 -0
  7. snk_cli-0.0.1/pyproject.toml +104 -0
  8. snk_cli-0.0.1/src/snk_cli/__about__.py +4 -0
  9. snk_cli-0.0.1/src/snk_cli/__init__.py +5 -0
  10. snk_cli-0.0.1/src/snk_cli/cli.py +231 -0
  11. snk_cli-0.0.1/src/snk_cli/config/__init__.py +1 -0
  12. snk_cli-0.0.1/src/snk_cli/config/config.py +219 -0
  13. snk_cli-0.0.1/src/snk_cli/config/utils.py +41 -0
  14. snk_cli-0.0.1/src/snk_cli/dynamic_typer.py +249 -0
  15. snk_cli-0.0.1/src/snk_cli/options/__init__.py +1 -0
  16. snk_cli-0.0.1/src/snk_cli/options/option.py +18 -0
  17. snk_cli-0.0.1/src/snk_cli/options/utils.py +117 -0
  18. snk_cli-0.0.1/src/snk_cli/subcommands/__init__.py +5 -0
  19. snk_cli-0.0.1/src/snk_cli/subcommands/config.py +64 -0
  20. snk_cli-0.0.1/src/snk_cli/subcommands/env.py +211 -0
  21. snk_cli-0.0.1/src/snk_cli/subcommands/profile.py +60 -0
  22. snk_cli-0.0.1/src/snk_cli/subcommands/run.py +471 -0
  23. snk_cli-0.0.1/src/snk_cli/subcommands/script.py +134 -0
  24. snk_cli-0.0.1/src/snk_cli/subcommands/utils.py +175 -0
  25. snk_cli-0.0.1/src/snk_cli/testing.py +19 -0
  26. snk_cli-0.0.1/src/snk_cli/utils.py +149 -0
  27. snk_cli-0.0.1/src/snk_cli/validate.py +66 -0
  28. snk_cli-0.0.1/src/snk_cli/workflow.py +177 -0
  29. snk_cli-0.0.1/tests/__init__.py +3 -0
  30. snk_cli-0.0.1/tests/conftest.py +38 -0
  31. snk_cli-0.0.1/tests/data/artic_v4.1.bed +209 -0
  32. snk_cli-0.0.1/tests/data/config.yaml +43 -0
  33. snk_cli-0.0.1/tests/data/cov.fasta +2998 -0
  34. snk_cli-0.0.1/tests/data/print_config/Snakefile +3 -0
  35. snk_cli-0.0.1/tests/data/print_config/cli.py +6 -0
  36. snk_cli-0.0.1/tests/data/print_config/config.yaml +1 -0
  37. snk_cli-0.0.1/tests/data/print_config/snk.yaml +3 -0
  38. snk_cli-0.0.1/tests/data/workflow/cli.py +6 -0
  39. snk_cli-0.0.1/tests/data/workflow/config.yaml +3 -0
  40. snk_cli-0.0.1/tests/data/workflow/resources/data.txt +1 -0
  41. snk_cli-0.0.1/tests/data/workflow/snk.yaml +56 -0
  42. snk_cli-0.0.1/tests/data/workflow/things/__about__.py +1 -0
  43. snk_cli-0.0.1/tests/data/workflow/workflow/Snakefile +25 -0
  44. snk_cli-0.0.1/tests/data/workflow/workflow/envs/python.yml +5 -0
  45. snk_cli-0.0.1/tests/data/workflow/workflow/profiles/base/config.yaml +1 -0
  46. snk_cli-0.0.1/tests/data/workflow/workflow/profiles/slurm/config.yaml +33 -0
  47. snk_cli-0.0.1/tests/data/workflow/workflow/scripts/hello.py +1 -0
  48. snk_cli-0.0.1/tests/test_cli/__init__.py +3 -0
  49. snk_cli-0.0.1/tests/test_cli/test_dynamic_options.py +67 -0
  50. snk_cli-0.0.1/tests/test_cli/test_run.py +40 -0
  51. snk_cli-0.0.1/tests/test_cli/test_snk_config.py +17 -0
  52. snk_cli-0.0.1/tests/test_cli/test_subcommands.py +24 -0
  53. snk_cli-0.0.1/tests/test_cli/test_validate.py +109 -0
  54. snk_cli-0.0.1/tests/test_cli/test_workflow_cli.py +82 -0
  55. snk_cli-0.0.1/tests/test_dynamic_typer.py +131 -0
  56. snk_cli-0.0.1/tests/utils.py +7 -0
@@ -0,0 +1,28 @@
1
+ name: publish
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*.*.*'
6
+ workflow_dispatch:
7
+ permissions:
8
+ contents: write
9
+ jobs:
10
+ pypi:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+ - uses: actions/setup-python@v4
15
+ with:
16
+ python-version: 3.x
17
+ - uses: actions/cache@v2
18
+ with:
19
+ key: ${{ github.ref }}
20
+ path: .cache
21
+ - name: Install hatch
22
+ run: pip install hatch
23
+ - name: Build & publish
24
+ env:
25
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
26
+ run: |
27
+ hatch build
28
+ hatch publish -u __token__ -a $PYPI_TOKEN
@@ -0,0 +1,38 @@
1
+
2
+ name: tests
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ - main
8
+ permissions:
9
+ contents: write
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ with:
16
+ fetch-depth: 0
17
+ - uses: "conda-incubator/setup-miniconda@v2"
18
+ with:
19
+ python-version: 3.11
20
+ auto-activate-base: false
21
+ miniforge-variant: Mambaforge
22
+ channels: conda-forge,bioconda,defaults
23
+ channel-priority: strict
24
+ auto-update-conda: true
25
+ - uses: actions/cache@v2
26
+ with:
27
+ key: ${{ github.ref }}
28
+ path: .cache
29
+ - run: pip install hatch
30
+ - run: sudo apt-get update && sudo apt-get install -y llvm
31
+ - run: sudo apt-get -y install graphviz
32
+ - name: Run tests
33
+ shell: bash
34
+ run: hatch run cov
35
+ - name: Upload coverage reports to Codecov
36
+ uses: codecov/codecov-action@v3
37
+
38
+
@@ -0,0 +1,13 @@
1
+ /.coverage
2
+ /.pytest_cache
3
+ /dist
4
+ *.pyc
5
+ .snakemake
6
+ reads
7
+ Note.md
8
+ tests/data/workflow/bin/workflow
9
+ hello.txt
10
+ __pycache__
11
+ tests/data/workflow/.conda
12
+ .DS_Store
13
+ .conda
@@ -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.
snk_cli-0.0.1/PKG-INFO ADDED
@@ -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 @@
1
+ # snk-cli
@@ -0,0 +1,104 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "snk-cli"
7
+ dynamic = ["version"]
8
+ description = ''
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ keywords = []
13
+ authors = [
14
+ { name = "Wytamma Wirth", email = "wytamma.wirth@me.com" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: Implementation :: CPython",
25
+ "Programming Language :: Python :: Implementation :: PyPy",
26
+ ]
27
+ dependencies = [
28
+ "snakemake>=7,<9",
29
+ "typer[all]~=0.9.0",
30
+ "GitPython~=3.1",
31
+ "pulp<2.8", # Pin pulp <2.8 for snakemake: https://github.com/snakemake/snakemake/issues/2607
32
+ "art~=5.9",
33
+ "makefun~=1.15",
34
+ "datrie>=0.8.2",
35
+ ]
36
+
37
+ [project.urls]
38
+ Documentation = "https://github.com/unknown/snk-cli#readme"
39
+ Issues = "https://github.com/unknown/snk-cli/issues"
40
+ Source = "https://github.com/unknown/snk-cli"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/snk_cli/__about__.py"
44
+
45
+
46
+ [[tool.hatch.envs.test.matrix]]
47
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
48
+ snakemake = ["7.32.4"]
49
+
50
+ # [[tool.hatch.envs.test.matrix]]
51
+ # python = ["3.11", "3.12"]
52
+ # snakemake = ["8.5.5"]
53
+
54
+ [tool.hatch.envs.test]
55
+ dependencies = [
56
+ "coverage[toml]>=6.5",
57
+ "pytest",
58
+ "snakemake=={matrix:snakemake}"]
59
+
60
+ [tool.hatch.envs.default]
61
+ dependencies = [
62
+ "coverage[toml]>=6.5",
63
+ "pytest",
64
+ ]
65
+ [tool.hatch.envs.default.scripts]
66
+ test = "pytest {args:tests}"
67
+ test-cov = "coverage run -m pytest {args:tests}"
68
+ cov-report = [
69
+ "- coverage combine",
70
+ "coverage report",
71
+ ]
72
+ cov = [
73
+ "test-cov",
74
+ "cov-report",
75
+ ]
76
+
77
+ [[tool.hatch.envs.all.matrix]]
78
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
79
+
80
+ [tool.hatch.envs.types]
81
+ dependencies = [
82
+ "mypy>=1.0.0",
83
+ ]
84
+ [tool.hatch.envs.types.scripts]
85
+ check = "mypy --install-types --non-interactive {args:src/snk_cli tests}"
86
+
87
+ [tool.coverage.run]
88
+ source_pkgs = ["snk_cli", "tests"]
89
+ branch = true
90
+ parallel = true
91
+ omit = [
92
+ "src/snk_cli/__about__.py",
93
+ ]
94
+
95
+ [tool.coverage.paths]
96
+ snk_cli = ["src/snk_cli", "*/snk-cli/src/snk_cli"]
97
+ tests = ["tests", "*/snk-cli/tests"]
98
+
99
+ [tool.coverage.report]
100
+ exclude_lines = [
101
+ "no cov",
102
+ "if __name__ == .__main__.:",
103
+ "if TYPE_CHECKING:",
104
+ ]
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2024-present Wytamma Wirth <wytamma.wirth@me.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.0.1"
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2024-present Wytamma Wirth <wytamma.wirth@me.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ from .cli import CLI
5
+ from .validate import validate_config
@@ -0,0 +1,231 @@
1
+ import inspect
2
+ import platform
3
+
4
+ import typer
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ import os
8
+
9
+
10
+ from snakemake import SNAKEFILE_CHOICES
11
+ from art import text2art
12
+
13
+ from snk_cli.dynamic_typer import DynamicTyper
14
+ from snk_cli.subcommands import EnvApp, ConfigApp, RunApp, ScriptApp, ProfileApp
15
+
16
+ from snk_cli.config.config import (
17
+ SnkConfig,
18
+ load_workflow_snakemake_config,
19
+ )
20
+ from snk_cli.options.utils import build_dynamic_cli_options
21
+ from snk_cli.workflow import Workflow
22
+
23
+
24
+ class CLI(DynamicTyper):
25
+ """
26
+ Constructor for the dynamic Snk CLI class.
27
+ Args:
28
+ workflow_dir_path (Path): Path to the workflow directory.
29
+ Side Effects:
30
+ Initializes the CLI class.
31
+ Examples:
32
+ >>> CLI(Path('/path/to/workflow'))
33
+ """
34
+
35
+ def __init__(self, workflow_dir_path: Path = None, *, pipeline_dir_path: Path = None, snk_config: SnkConfig = None) -> None:
36
+ if pipeline_dir_path is not None:
37
+ # raise a deprecation warning
38
+ import warnings
39
+ warnings.warn(
40
+ "The `pipeline_dir_path` argument is deprecated and will be removed in a future release. Use `workflow_dir_path` instead.",
41
+ DeprecationWarning,
42
+ )
43
+ workflow_dir_path = pipeline_dir_path
44
+ if workflow_dir_path is None:
45
+ # get the calling frame (the frame of the function that called this function)
46
+ calling_frame = inspect.currentframe().f_back
47
+ # get the file path from the calling frame
48
+ workflow_dir_path = Path(calling_frame.f_globals["__file__"])
49
+ else:
50
+ workflow_dir_path = Path(workflow_dir_path)
51
+ if workflow_dir_path.is_file():
52
+ workflow_dir_path = workflow_dir_path.parent
53
+ self.workflow = Workflow(path=workflow_dir_path)
54
+ self.snakemake_config = load_workflow_snakemake_config(workflow_dir_path)
55
+ if snk_config is None:
56
+ self.snk_config = SnkConfig.from_workflow_dir(
57
+ workflow_dir_path, create_if_not_exists=True
58
+ )
59
+ else:
60
+ self.snk_config = snk_config
61
+ if self.snk_config.version:
62
+ self.version = self.snk_config.version
63
+ else:
64
+ self.version = self.workflow.version
65
+ self.options = build_dynamic_cli_options(self.snakemake_config, self.snk_config)
66
+ self.snakefile = self._find_snakefile()
67
+ self.conda_prefix_dir = self.workflow.conda_prefix_dir
68
+ self.singularity_prefix_dir = self.workflow.singularity_prefix_dir
69
+ self.name = self.workflow.name
70
+ self.verbose = False
71
+ if (
72
+ platform.system() == "Darwin"
73
+ and platform.processor() == "arm"
74
+ and not os.environ.get("CONDA_SUBDIR")
75
+ ):
76
+ os.environ["CONDA_SUBDIR"] = "osx-64"
77
+
78
+ # dynamically create the logo
79
+ self.logo = self._create_logo(
80
+ tagline=self.snk_config.tagline, font=self.snk_config.font
81
+ )
82
+ callback = self._create_callback()
83
+ callback.__doc__ = self.logo
84
+
85
+ # registration
86
+ self.register_callback(
87
+ callback,
88
+ invoke_without_command=True,
89
+ context_settings={"help_option_names": ["-h", "--help"]},
90
+ )
91
+ self.register_command(self.info, help="Show information about the workflow.")
92
+
93
+ run_app = RunApp(
94
+ conda_prefix_dir=self.conda_prefix_dir,
95
+ snk_config=self.snk_config,
96
+ singularity_prefix_dir=self.singularity_prefix_dir,
97
+ snakefile=self.snakefile,
98
+ workflow=self.workflow,
99
+ verbose=self.verbose,
100
+ logo=self.logo,
101
+ dynamic_run_options=self.options,
102
+ )
103
+ # Subcommands
104
+ self.register_command(
105
+ run_app,
106
+ name="run",
107
+ )
108
+ self.register_command(
109
+ ConfigApp(
110
+ workflow=self.workflow,
111
+ options=self.options,
112
+ ),
113
+ name="config",
114
+ )
115
+ if self.workflow.environments:
116
+ self.register_group(
117
+ EnvApp(
118
+ workflow=self.workflow,
119
+ conda_prefix_dir=self.conda_prefix_dir,
120
+ snakemake_config=self.snakemake_config,
121
+ snakefile=self.snakefile,
122
+ ),
123
+ name="env",
124
+ help="Access the workflow conda environments.",
125
+ )
126
+ if self.workflow.scripts:
127
+ self.register_group(
128
+ ScriptApp(
129
+ workflow=self.workflow,
130
+ conda_prefix_dir=self.conda_prefix_dir,
131
+ snakemake_config=self.snakemake_config,
132
+ snakefile=self.snakefile,
133
+ ),
134
+ name="script",
135
+ help="Access the workflow scripts.",
136
+ )
137
+ if self.workflow.profiles:
138
+ self.register_group(
139
+ ProfileApp(
140
+ workflow=self.workflow,
141
+ ),
142
+ name="profile",
143
+ help="Access the workflow profiles.",
144
+ )
145
+
146
+ def _print_pipline_version(self, ctx: typer.Context, value: bool):
147
+ if value:
148
+ typer.echo(self.version)
149
+ raise typer.Exit()
150
+
151
+ def _print_pipline_path(self, ctx: typer.Context, value: bool):
152
+ if value:
153
+ typer.echo(self.workflow.path)
154
+ raise typer.Exit()
155
+
156
+ def _create_callback(self):
157
+ def callback(
158
+ ctx: typer.Context,
159
+ version: Optional[bool] = typer.Option(
160
+ None,
161
+ "-v",
162
+ "--version",
163
+ help="Show the workflow version and exit.",
164
+ is_eager=True,
165
+ callback=self._print_pipline_version,
166
+ show_default=False,
167
+ ),
168
+ path: Optional[bool] = typer.Option(
169
+ None,
170
+ "-p",
171
+ "--path",
172
+ help="Show the workflow path and exit.",
173
+ is_eager=True,
174
+ callback=self._print_pipline_path,
175
+ show_default=False,
176
+ ),
177
+ ):
178
+ if ctx.invoked_subcommand is None:
179
+ typer.echo(f"{ctx.get_help()}")
180
+
181
+ return callback
182
+
183
+ def _create_logo(
184
+ self, tagline="A Snakemake workflow CLI generated with snk", font="small"
185
+ ):
186
+ """
187
+ Create a logo for the CLI.
188
+ Args:
189
+ font (str): The font to use for the logo.
190
+ Returns:
191
+ str: The logo.
192
+ Examples:
193
+ >>> CLI._create_logo()
194
+ """
195
+ if self.snk_config.art:
196
+ art = self.snk_config.art
197
+ else:
198
+ logo = self.snk_config.logo if self.snk_config.logo else self.name
199
+ art = text2art(logo, font=font)
200
+ doc = f"""\b{art}\b{tagline}"""
201
+ return doc
202
+
203
+ def _find_snakefile(self):
204
+ """
205
+ Search possible snakefile locations.
206
+ Returns:
207
+ Path: The path to the snakefile.
208
+ Examples:
209
+ >>> CLI._find_snakefile()
210
+ """
211
+ for path in SNAKEFILE_CHOICES:
212
+ if (self.workflow.path / path).exists():
213
+ return self.workflow.path / path
214
+ raise FileNotFoundError("Snakefile not found!")
215
+
216
+ def info(self):
217
+ """
218
+ Display information about current workflow install.
219
+ Returns:
220
+ str: A JSON string containing information about the current workflow install.
221
+ Examples:
222
+ >>> CLI.info()
223
+ """
224
+ import json
225
+
226
+ info_dict = {}
227
+ info_dict["name"] = self.workflow.path.name
228
+ info_dict["version"] = self.version
229
+ info_dict["workflow_dir_path"] = str(self.workflow.path)
230
+ typer.echo(json.dumps(info_dict, indent=2))
231
+
@@ -0,0 +1 @@
1
+ from .config import SnkConfig
@@ -0,0 +1,219 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional
3
+ import snakemake
4
+ from dataclasses import dataclass, field
5
+ from .utils import get_version_from_config
6
+ import yaml
7
+
8
+
9
+ class SnkConfigError(Exception):
10
+ """Base class for all SNK config exceptions"""
11
+
12
+ class InvalidSnkConfigError(SnkConfigError, ValueError):
13
+ """Thrown if the given SNK config appears to have an invalid format."""
14
+
15
+ class MissingSnkConfigError(SnkConfigError, FileNotFoundError):
16
+ """Thrown if the given SNK config file cannot be found."""
17
+
18
+ @dataclass
19
+ class SnkConfig:
20
+ """
21
+ A dataclass for storing Snakemake workflow configuration.
22
+ """
23
+
24
+ art: str = None
25
+ logo: str = None
26
+ tagline: str = "A Snakemake workflow CLI generated with Snk"
27
+ font: str = "small"
28
+ version: Optional[str] = None
29
+ conda: bool = True
30
+ resources: List[Path] = field(default_factory=list)
31
+ skip_missing: bool = False # skip any missing cli options (i.e. those not in the snk file)
32
+ additional_snakemake_args: List[str] = field(default_factory=list)
33
+ cli: dict = field(default_factory=dict)
34
+ symlink_resources: bool = False
35
+ _snk_config_path: Path = None
36
+
37
+ @classmethod
38
+ def from_path(cls, snk_config_path: Path):
39
+ """
40
+ Load and validate Snk config from file.
41
+ Args:
42
+ snk_config_path (Path): Path to the SNK config file.
43
+ Returns:
44
+ SnkConfig: A SnkConfig object.
45
+ Raises:
46
+ FileNotFoundError: If the SNK config file is not found.
47
+ Examples:
48
+ >>> SnkConfig.from_path(Path("snk.yaml"))
49
+ SnkConfig(art=None, logo=None, tagline='A Snakemake workflow CLI generated with Snk', font='small', resources=[], annotations={}, symlink_resources=False, _snk_config_path=PosixPath('snk.yaml'))
50
+ """
51
+ if not snk_config_path.exists():
52
+ raise MissingSnkConfigError(
53
+ f"Could not find SNK config file: {snk_config_path}"
54
+ ) from FileNotFoundError
55
+ # raise error if file is empty
56
+ if snk_config_path.stat().st_size == 0:
57
+ raise InvalidSnkConfigError(f"SNK config file is empty: {snk_config_path}") from ValueError
58
+
59
+ snk_config_dict = snakemake.load_configfile(snk_config_path)
60
+ snk_config_dict["version"] = get_version_from_config(snk_config_path, snk_config_dict)
61
+ if "annotations" in snk_config_dict:
62
+ # TODO: remove annotations in the future
63
+ snk_config_dict["cli"] = snk_config_dict["annotations"]
64
+ del snk_config_dict["annotations"]
65
+ if "conda_required" in snk_config_dict:
66
+ # TODO: remove conda_required in the future
67
+ snk_config_dict["conda"] = snk_config_dict["conda_required"]
68
+ del snk_config_dict["conda_required"]
69
+ snk_config = cls(**snk_config_dict)
70
+ snk_config.resources = [
71
+ snk_config_path.parent / resource for resource in snk_config.resources
72
+ ]
73
+ snk_config.validate_resources(snk_config.resources)
74
+ snk_config._snk_config_path = snk_config_path
75
+ return snk_config
76
+
77
+ @classmethod
78
+ def from_workflow_dir(
79
+ cls, workflow_dir_path: Path, create_if_not_exists: bool = False
80
+ ):
81
+ """
82
+ Load and validate SNK config from workflow directory.
83
+ Args:
84
+ workflow_dir_path (Path): Path to the workflow directory.
85
+ create_if_not_exists (bool): Whether to create a SNK config file if one does not exist.
86
+ Returns:
87
+ SnkConfig: A SnkConfig object.
88
+ Raises:
89
+ FileNotFoundError: If the SNK config file is not found.
90
+ Examples:
91
+ >>> SnkConfig.from_workflow_dir(Path("workflow"))
92
+ SnkConfig(art=None, logo=None, tagline='A Snakemake workflow CLI generated with Snk', font='small', resources=[], annotations={}, symlink_resources=False, _snk_config_path=PosixPath('workflow/snk.yaml'))
93
+ """
94
+ if (workflow_dir_path / "snk.yaml").exists():
95
+ return cls.from_path(workflow_dir_path / "snk.yaml")
96
+ elif (workflow_dir_path / ".snk").exists():
97
+ import warnings
98
+
99
+ warnings.warn(
100
+ "Use of .snk will be deprecated in the future. Please use snk.yaml instead.",
101
+ DeprecationWarning,
102
+ )
103
+ return cls.from_path(workflow_dir_path / ".snk")
104
+ elif create_if_not_exists:
105
+ snk_config = cls(_snk_config_path=workflow_dir_path / "snk.yaml")
106
+ return snk_config
107
+ else:
108
+ raise FileNotFoundError(
109
+ f"Could not find SNK config file in workflow directory: {workflow_dir_path}"
110
+ )
111
+
112
+ def validate_resources(self, resources):
113
+ """
114
+ Validate resources.
115
+ Args:
116
+ resources (List[Path]): List of resources to validate.
117
+ Raises:
118
+ FileNotFoundError: If a resource is not found.
119
+ Notes:
120
+ This function does not modify the resources list.
121
+ Examples:
122
+ >>> SnkConfig.validate_resources([Path("resource1.txt"), Path("resource2.txt")])
123
+ """
124
+ for resource in resources:
125
+ if not resource.exists():
126
+ raise FileNotFoundError(f"Could not find resource: {resource}")
127
+
128
+ def add_resources(self, resources: List[Path], workflow_dir_path: Path = None):
129
+ """
130
+ Add resources to the SNK config.
131
+ Args:
132
+ resources (List[Path]): List of resources to add.
133
+ workflow_dir_path (Path): Path to the workflow directory.
134
+ Returns:
135
+ None
136
+ Side Effects:
137
+ Adds the resources to the SNK config.
138
+ Examples:
139
+ >>> snk_config = SnkConfig()
140
+ >>> snk_config.add_resources([Path("resource1.txt"), Path("resource2.txt")], Path("workflow"))
141
+ """
142
+ processed = []
143
+ for resource in resources:
144
+ if workflow_dir_path and not resource.is_absolute():
145
+ resource = workflow_dir_path / resource
146
+ processed.append(resource)
147
+ self.validate_resources(processed)
148
+ self.resources.extend(processed)
149
+
150
+ def to_yaml(self, path: Path) -> None:
151
+ """
152
+ Write SNK config to YAML file.
153
+ Args:
154
+ path (Path): Path to write the YAML file to.
155
+ Returns:
156
+ None
157
+ Side Effects:
158
+ Writes the SNK config to the specified path.
159
+ Examples:
160
+ >>> snk_config = SnkConfig()
161
+ >>> snk_config.to_yaml(Path("snk.yaml"))
162
+ """
163
+ config_dict = {k: v for k, v in vars(self).items() if not k.startswith("_")}
164
+ with open(path, "w") as f:
165
+ yaml.dump(config_dict, f)
166
+
167
+ def save(self) -> None:
168
+ """
169
+ Save SNK config.
170
+ Args:
171
+ path (Path): Path to write the YAML file to.
172
+ Returns:
173
+ None
174
+ Side Effects:
175
+ Writes the SNK config to the path specified by _snk_config_path.
176
+ Examples:
177
+ >>> snk_config = SnkConfig()
178
+ >>> snk_config.save()
179
+ """
180
+ self.to_yaml(self._snk_config_path)
181
+
182
+
183
+ def get_config_from_workflow_dir(workflow_dir_path: Path):
184
+ """
185
+ Get the config file from a workflow directory.
186
+ Args:
187
+ workflow_dir_path (Path): Path to the workflow directory.
188
+ Returns:
189
+ Path: Path to the config file, or None if not found.
190
+ Examples:
191
+ >>> get_config_from_workflow_dir(Path("workflow"))
192
+ PosixPath('workflow/config.yaml')
193
+ """
194
+ for path in [
195
+ Path("config") / "config.yaml",
196
+ Path("config") / "config.yml",
197
+ "config.yaml",
198
+ "config.yml",
199
+ ]:
200
+ if (workflow_dir_path / path).exists():
201
+ return workflow_dir_path / path
202
+ return None
203
+
204
+
205
+ def load_workflow_snakemake_config(workflow_dir_path: Path):
206
+ """
207
+ Load the Snakemake config from a workflow directory.
208
+ Args:
209
+ workflow_dir_path (Path): Path to the workflow directory.
210
+ Returns:
211
+ dict: The Snakemake config.
212
+ Examples:
213
+ >>> load_workflow_snakemake_config(Path("workflow"))
214
+ {'inputs': {'data': 'data.txt'}, 'outputs': {'results': 'results.txt'}}
215
+ """
216
+ workflow_config_path = get_config_from_workflow_dir(workflow_dir_path)
217
+ if not workflow_config_path or not workflow_config_path.exists():
218
+ return {}
219
+ return snakemake.load_configfile(workflow_config_path)