snk-cli 0.5.1__tar.gz → 0.5.3__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 (70) hide show
  1. {snk_cli-0.5.1 → snk_cli-0.5.3}/PKG-INFO +1 -1
  2. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/__about__.py +1 -1
  3. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/cli.py +1 -1
  4. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/config/config.py +11 -0
  5. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/dynamic_typer.py +1 -1
  6. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/config.py +1 -1
  7. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/env.py +1 -1
  8. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/profile.py +1 -1
  9. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/run.py +16 -23
  10. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/script.py +1 -1
  11. snk_cli-0.5.3/tests/test_SnkConfig.py +125 -0
  12. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_validate.py +6 -0
  13. {snk_cli-0.5.1 → snk_cli-0.5.3}/.github/workflows/publish.yml +0 -0
  14. {snk_cli-0.5.1 → snk_cli-0.5.3}/.github/workflows/tests.yml +0 -0
  15. {snk_cli-0.5.1 → snk_cli-0.5.3}/.gitignore +0 -0
  16. {snk_cli-0.5.1 → snk_cli-0.5.3}/LICENSE.txt +0 -0
  17. {snk_cli-0.5.1 → snk_cli-0.5.3}/README.md +0 -0
  18. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/index.md +0 -0
  19. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/cli.md +0 -0
  20. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/config.md +0 -0
  21. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/dynamic_typer.md +0 -0
  22. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/options.md +0 -0
  23. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/subcommands.md +0 -0
  24. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/testing.md +0 -0
  25. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/utils.md +0 -0
  26. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/validate.md +0 -0
  27. {snk_cli-0.5.1 → snk_cli-0.5.3}/docs/reference/workflow.md +0 -0
  28. {snk_cli-0.5.1 → snk_cli-0.5.3}/mkdocs.yml +0 -0
  29. {snk_cli-0.5.1 → snk_cli-0.5.3}/pyproject.toml +0 -0
  30. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/__init__.py +0 -0
  31. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/conda.py +0 -0
  32. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/config/__init__.py +0 -0
  33. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/config/utils.py +0 -0
  34. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/options/__init__.py +0 -0
  35. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/options/option.py +0 -0
  36. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/options/utils.py +0 -0
  37. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/subcommands/__init__.py +0 -0
  38. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/testing.py +0 -0
  39. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/utils.py +0 -0
  40. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/validate.py +0 -0
  41. {snk_cli-0.5.1 → snk_cli-0.5.3}/src/snk_cli/workflow.py +0 -0
  42. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/__init__.py +0 -0
  43. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/conftest.py +0 -0
  44. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/artic_v4.1.bed +0 -0
  45. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/config.yaml +0 -0
  46. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/cov.fasta +0 -0
  47. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/print_config/Snakefile +0 -0
  48. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/print_config/cli.py +0 -0
  49. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/print_config/config.yaml +0 -0
  50. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/print_config/snk.yaml +0 -0
  51. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/cli.py +0 -0
  52. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/config.yaml +0 -0
  53. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/resources/data.txt +0 -0
  54. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/snk.yaml +0 -0
  55. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/things/__about__.py +0 -0
  56. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/workflow/Snakefile +0 -0
  57. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/workflow/envs/wget.yml +0 -0
  58. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/workflow/profiles/base/config.yaml +0 -0
  59. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/workflow/profiles/slurm/config.yaml +0 -0
  60. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/data/workflow/workflow/scripts/hello.py +0 -0
  61. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/__init__.py +0 -0
  62. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_dynamic_options.py +0 -0
  63. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_profile.py +0 -0
  64. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_run.py +0 -0
  65. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_snk_config.py +0 -0
  66. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_subcommands.py +0 -0
  67. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_cli/test_workflow_cli.py +0 -0
  68. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_conda_env.py +0 -0
  69. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/test_dynamic_typer.py +0 -0
  70. {snk_cli-0.5.1 → snk_cli-0.5.3}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snk-cli
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Project-URL: Documentation, https://github.com/wytamma/snk-cli#readme
5
5
  Project-URL: Issues, https://github.com/wytamma/snk-cli/issues
6
6
  Project-URL: Source, https://github.com/wytamma/snk-cli
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Wytamma Wirth <wytamma.wirth@me.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.5.1"
4
+ __version__ = "0.5.3"
@@ -259,5 +259,5 @@ class CLI(DynamicTyper):
259
259
  info_dict["conda_prefix_dir"] = str(self.conda_prefix_dir)
260
260
  info_dict["singularity_prefix_dir"] = str(self.singularity_prefix_dir)
261
261
  info_dict["workflow_dir_path"] = str(self.workflow.path)
262
- typer.echo(json.dumps(info_dict, indent=2))
262
+ self.echo(json.dumps(info_dict, indent=2))
263
263
 
@@ -205,6 +205,17 @@ class SnkConfig:
205
205
  >>> snk_config.to_yaml(Path("snk.yaml"))
206
206
  """
207
207
  config_dict = {k: v for k, v in vars(self).items() if not k.startswith("_")}
208
+ def convert_paths(data):
209
+ if isinstance(data, dict):
210
+ return {key: convert_paths(value) for key, value in data.items()}
211
+ elif isinstance(data, list):
212
+ return [convert_paths(item) for item in data]
213
+ elif isinstance(data, Path):
214
+ return str(data)
215
+ return data
216
+
217
+ config_dict = convert_paths(config_dict)
218
+
208
219
  with open(path, "w") as f:
209
220
  yaml.dump(config_dict, f)
210
221
 
@@ -152,7 +152,7 @@ class DynamicTyper:
152
152
  if option.type is Enum or option.choices:
153
153
  if not option.choices:
154
154
  raise ValueError(f"Enum type {option.name} requires choices to be defined.")
155
- annotation_type = Enum(f'{option.name}Enum', {e: e for e in option.choices})
155
+ annotation_type = Enum(f'{option.name}', {str(e): str(e) for e in option.choices})
156
156
  if default:
157
157
  try:
158
158
  default = annotation_type(default)
@@ -66,4 +66,4 @@ class ConfigApp(DynamicTyper):
66
66
  console = Console()
67
67
  console.print(syntax)
68
68
  else:
69
- typer.echo(yaml_str)
69
+ self.echo(yaml_str)
@@ -106,7 +106,7 @@ class EnvApp(DynamicTyper):
106
106
  console = Console()
107
107
  console.print(syntax)
108
108
  else:
109
- typer.echo(env_file_text)
109
+ self.echo(env_file_text)
110
110
 
111
111
  def run(
112
112
  self,
@@ -65,7 +65,7 @@ class ProfileApp(DynamicTyper):
65
65
  console = Console()
66
66
  console.print(syntax)
67
67
  else:
68
- typer.echo(profile_file_text)
68
+ self.echo(profile_file_text)
69
69
 
70
70
  def _open_text_editor(self, file_path):
71
71
  """
@@ -213,10 +213,9 @@ class RunApp(DynamicTyper):
213
213
  # Set up conda frontend
214
214
  conda_found = check_command_available("conda")
215
215
  if not conda_found and verbose:
216
- typer.secho(
216
+ self.log(
217
217
  "Conda not found! Install conda to use environments.\n",
218
- fg=typer.colors.MAGENTA,
219
- err=True,
218
+ color=typer.colors.MAGENTA,
220
219
  )
221
220
 
222
221
  if conda_found and self.snk_config.conda and not no_conda:
@@ -228,10 +227,9 @@ class RunApp(DynamicTyper):
228
227
  )
229
228
  if not check_command_available("mamba"):
230
229
  if verbose:
231
- typer.secho(
230
+ self.log(
232
231
  "Could not find mamba, using conda instead...",
233
- fg=typer.colors.MAGENTA,
234
- err=True,
232
+ color=typer.colors.MAGENTA,
235
233
  )
236
234
  args.append("--conda-frontend=conda")
237
235
  else:
@@ -260,7 +258,7 @@ class RunApp(DynamicTyper):
260
258
  if configs:
261
259
  args.extend(["--config", *configs])
262
260
  if verbose:
263
- typer.secho(f"snakemake {' '.join(args)}\n", fg=typer.colors.MAGENTA, err=True)
261
+ self.log(f"snakemake {' '.join(args)}\n", color=typer.colors.MAGENTA)
264
262
  if not keep_snakemake and Path(".snakemake").exists():
265
263
  keep_snakemake = True
266
264
  try:
@@ -281,7 +279,7 @@ class RunApp(DynamicTyper):
281
279
  if status:
282
280
  sys.exit(status)
283
281
  if not keep_snakemake and Path(".snakemake").exists():
284
- typer.secho("Cleaning up '.snakemake' folder... use --keep-snakemake to keep.", fg=typer.colors.YELLOW, err=True)
282
+ self.log("Cleaning up '.snakemake' folder... use --keep-snakemake to keep.")
285
283
  shutil.rmtree(".snakemake")
286
284
 
287
285
  def _save_dag(self, snakemake_args: List[str], filename: Path):
@@ -320,11 +318,10 @@ class RunApp(DynamicTyper):
320
318
  )
321
319
  with open(filename, "w") as output_file:
322
320
  if self.verbose:
323
- typer.secho(f"Saving dag to {filename}", fg=typer.colors.MAGENTA, err=True)
321
+ self.log(f"Saving dag to {filename}", color=typer.colors.MAGENTA)
324
322
  subprocess.run(["cat"], stdin=dot_process.stdout, stdout=output_file)
325
323
  except (subprocess.CalledProcessError, FileNotFoundError):
326
- typer.echo("dot command not found!", fg=typer.colors.RED, err=True)
327
- raise typer.Exit(1)
324
+ self.error("Graphviz `dot` command not found! Please install.", exit=True)
328
325
 
329
326
  @contextmanager
330
327
  def _copy_resources(
@@ -355,10 +352,9 @@ class RunApp(DynamicTyper):
355
352
 
356
353
  def copy_resource(src: Path, dst: Path, symlink: bool = False):
357
354
  if self.verbose:
358
- typer.secho(
355
+ self.log(
359
356
  f" - Copying resource '{src}' to '{dst}'",
360
- fg=typer.colors.MAGENTA,
361
- err=True,
357
+ color=typer.colors.MAGENTA,
362
358
  )
363
359
  target_is_directory = src.is_dir()
364
360
  if symlink:
@@ -380,10 +376,9 @@ class RunApp(DynamicTyper):
380
376
  if resources_folder.exists():
381
377
  resources.insert(0, Path("resources"))
382
378
  if self.verbose and resources:
383
- typer.secho(
379
+ self.log(
384
380
  f"Copying {len(resources)} resources to working directory...",
385
- fg=typer.colors.MAGENTA,
386
- err=True,
381
+ color=typer.colors.MAGENTA
387
382
  )
388
383
  try:
389
384
  for resource in resources:
@@ -394,10 +389,9 @@ class RunApp(DynamicTyper):
394
389
  copy_resource(abs_path, destination, symlink=symlink_resources)
395
390
  copied_resources.append(destination)
396
391
  elif self.verbose:
397
- typer.secho(
392
+ self.log(
398
393
  f" - Resource '{resource.name}' already exists! Skipping...",
399
- fg=typer.colors.MAGENTA,
400
- err=True,
394
+ color=typer.colors.MAGENTA,
401
395
  )
402
396
  yield
403
397
  finally:
@@ -406,10 +400,9 @@ class RunApp(DynamicTyper):
406
400
  for copied_resource in copied_resources:
407
401
  if copied_resource.exists():
408
402
  if self.verbose:
409
- typer.secho(
403
+ self.log(
410
404
  f"Deleting '{copied_resource.name}' resource...",
411
- fg=typer.colors.MAGENTA,
412
- err=True,
405
+ color=typer.colors.MAGENTA,
413
406
  )
414
407
  remove_resource(copied_resource)
415
408
 
@@ -79,7 +79,7 @@ class ScriptApp(DynamicTyper):
79
79
  console = Console()
80
80
  console.print(code)
81
81
  else:
82
- typer.echo(code)
82
+ self.echo(code)
83
83
 
84
84
  def _get_executor(self, suffix: str) -> str:
85
85
  if suffix == "py":
@@ -0,0 +1,125 @@
1
+ import pytest
2
+ from snk_cli.config.config import SnkConfig
3
+
4
+
5
+ def test_snk_config_creation():
6
+ snk_config = SnkConfig()
7
+ assert snk_config.tagline == "A Snakemake workflow CLI generated with Snk"
8
+ assert snk_config.font == "small"
9
+ assert snk_config.resources == []
10
+ assert snk_config.cli == {}
11
+ assert snk_config.symlink_resources is False
12
+ assert snk_config.version is None
13
+ assert snk_config.skip_missing is False
14
+
15
+
16
+ def test_validate_resources_with_existing_files(tmp_path):
17
+ resources = [tmp_path / "file1", tmp_path / "file2"]
18
+ for resource in resources:
19
+ resource.touch()
20
+ snk_config = SnkConfig()
21
+ # Should not raise an exception
22
+ snk_config.validate_resources(resources)
23
+
24
+
25
+ def test_validate_resources_with_missing_files(tmp_path):
26
+ resources = [tmp_path / "file1", tmp_path / "missing_file"]
27
+ resources[0].touch()
28
+ snk_config = SnkConfig()
29
+ with pytest.raises(FileNotFoundError):
30
+ snk_config.validate_resources(resources)
31
+
32
+
33
+ def test_add_resources(tmp_path):
34
+ resources = [tmp_path / "file1", tmp_path / "file2"]
35
+ for resource in resources:
36
+ resource.touch()
37
+ snk_config = SnkConfig()
38
+ snk_config.add_resources(resources)
39
+ assert len(snk_config.resources) == 2
40
+
41
+
42
+ def test_from_path_with_existing_file(tmp_path):
43
+ config_file = tmp_path / "snk.yaml"
44
+ config_file.touch()
45
+ config_file.write_text("logo: test_logo")
46
+ snk_config = SnkConfig.from_path(config_file)
47
+ assert snk_config._snk_config_path == config_file
48
+
49
+ def test_missing_file(tmp_path):
50
+ config_file = tmp_path / "snk.yaml"
51
+ with pytest.raises(FileNotFoundError):
52
+ SnkConfig.from_path(config_file)
53
+
54
+ def test_empty_file(tmp_path):
55
+ config_file = tmp_path / "snk.yaml"
56
+ config_file.touch()
57
+ with pytest.raises(ValueError):
58
+ SnkConfig.from_path(config_file)
59
+
60
+ def test_invalid_yaml(tmp_path):
61
+ config_file = tmp_path / "snk.yaml"
62
+ config_file.touch()
63
+ config_file.write_text("logo: test_logo\ninvalid_yaml")
64
+ with pytest.raises(Exception):
65
+ SnkConfig.from_path(config_file)
66
+
67
+ def test_from_dir_with_existing_file(tmp_path):
68
+ config_file = tmp_path / ".snk"
69
+ config_file.touch()
70
+ config_file.write_text("logo: test_logo")
71
+ # catch warning
72
+ with pytest.warns(DeprecationWarning):
73
+ snk_config = SnkConfig.from_workflow_dir(tmp_path)
74
+ assert snk_config._snk_config_path == config_file
75
+
76
+
77
+ def test_from_path_with_missing_file(tmp_path):
78
+ config_file = tmp_path / "missing_file.yaml"
79
+ # assert it raises FileNotFoundError
80
+ with pytest.raises(FileNotFoundError):
81
+ SnkConfig.from_path(config_file)
82
+
83
+
84
+ def test_to_yaml(tmp_path):
85
+ snk_config = SnkConfig(art="test_art", logo="test_logo")
86
+ yaml_file = tmp_path / "snk.yaml"
87
+ snk_config.to_yaml(yaml_file)
88
+ assert yaml_file.exists()
89
+
90
+
91
+ def test_save(tmp_path):
92
+ config_file = tmp_path / "snk.yaml"
93
+ snk_config = SnkConfig(_snk_config_path=config_file)
94
+ snk_config.art = "new_art"
95
+ snk_config.save()
96
+ saved_snk_config = SnkConfig.from_path(config_file)
97
+ assert saved_snk_config.art == "new_art"
98
+
99
+
100
+ def test_version_from_about_file(tmp_path):
101
+ config_file = tmp_path / "snk.yaml"
102
+ about_file = tmp_path / "__about__.py"
103
+ about_file.touch()
104
+ about_file.write_text("__version__ = '0.0.1'")
105
+ config_file.write_text("version: __about__.py")
106
+ snk_config = SnkConfig.from_path(config_file)
107
+ assert snk_config.version == "0.0.1"
108
+
109
+ def test_version_from_about_file_with_missing_file(tmp_path):
110
+ config_file = tmp_path / "snk.yaml"
111
+ about_file = tmp_path / "__about__.py"
112
+ config_file.write_text("version: __about__.py")
113
+ with pytest.raises(FileNotFoundError):
114
+ SnkConfig.from_path(config_file)
115
+
116
+ def test_paths_are_written_as_strings(tmp_path):
117
+ config_file = tmp_path / "snk.yaml"
118
+ snk_config = SnkConfig(snakefile=tmp_path / "snakefile", resources=[tmp_path / "file1", tmp_path / "file2"])
119
+ snk_config.to_yaml(config_file)
120
+ with open(config_file) as f:
121
+ config = f.read()
122
+ print(config)
123
+ assert f"snakefile: {tmp_path / 'snakefile'}" in config
124
+ assert f"- {tmp_path / 'file1'}" in config
125
+ assert f"- {tmp_path / 'file2'}" in config
@@ -24,6 +24,12 @@ import pytest
24
24
  {"example": {"type": "bool"}},
25
25
  {"example": True}
26
26
  ),
27
+ # timestamp
28
+ (
29
+ {"example": "2021-01-01T00:00:00"},
30
+ {"example": {"type": "str"}},
31
+ {"example": "2021-01-01T00:00:00"}
32
+ ),
27
33
  # None
28
34
  (
29
35
  {"example": None},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes