adagio-cli 0.1.0__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. adagio_cli-0.1.0/LICENSE +21 -0
  2. adagio_cli-0.1.0/PKG-INFO +136 -0
  3. adagio_cli-0.1.0/README.md +105 -0
  4. adagio_cli-0.1.0/pyproject.toml +91 -0
  5. adagio_cli-0.1.0/src/adagio/__init__.py +15 -0
  6. adagio_cli-0.1.0/src/adagio/app/parsers/pipeline.py +89 -0
  7. adagio_cli-0.1.0/src/adagio/check.py +16 -0
  8. adagio_cli-0.1.0/src/adagio/cli/__init__.py +1 -0
  9. adagio_cli-0.1.0/src/adagio/cli/args.py +62 -0
  10. adagio_cli-0.1.0/src/adagio/cli/cache.py +82 -0
  11. adagio_cli-0.1.0/src/adagio/cli/config.py +183 -0
  12. adagio_cli-0.1.0/src/adagio/cli/dynamic.py +656 -0
  13. adagio_cli-0.1.0/src/adagio/cli/main.py +313 -0
  14. adagio_cli-0.1.0/src/adagio/cli/pipeline.py +49 -0
  15. adagio_cli-0.1.0/src/adagio/cli/pipeline_sources.py +322 -0
  16. adagio_cli-0.1.0/src/adagio/cli/qapi.py +300 -0
  17. adagio_cli-0.1.0/src/adagio/cli/runner.py +310 -0
  18. adagio_cli-0.1.0/src/adagio/cli/runtime.py +530 -0
  19. adagio_cli-0.1.0/src/adagio/cli/task_exec.py +474 -0
  20. adagio_cli-0.1.0/src/adagio/convert.py +0 -0
  21. adagio_cli-0.1.0/src/adagio/describe.py +487 -0
  22. adagio_cli-0.1.0/src/adagio/executors/__init__.py +34 -0
  23. adagio_cli-0.1.0/src/adagio/executors/apptainer.py +275 -0
  24. adagio_cli-0.1.0/src/adagio/executors/base.py +92 -0
  25. adagio_cli-0.1.0/src/adagio/executors/cache_support.py +82 -0
  26. adagio_cli-0.1.0/src/adagio/executors/common.py +124 -0
  27. adagio_cli-0.1.0/src/adagio/executors/conda.py +314 -0
  28. adagio_cli-0.1.0/src/adagio/executors/container_support.py +260 -0
  29. adagio_cli-0.1.0/src/adagio/executors/defaults.py +99 -0
  30. adagio_cli-0.1.0/src/adagio/executors/docker.py +404 -0
  31. adagio_cli-0.1.0/src/adagio/executors/path_utils.py +60 -0
  32. adagio_cli-0.1.0/src/adagio/executors/serial_runner.py +318 -0
  33. adagio_cli-0.1.0/src/adagio/executors/signature.py +140 -0
  34. adagio_cli-0.1.0/src/adagio/executors/task_contract.py +125 -0
  35. adagio_cli-0.1.0/src/adagio/executors/task_environments.py +760 -0
  36. adagio_cli-0.1.0/src/adagio/model/__init__.py +0 -0
  37. adagio_cli-0.1.0/src/adagio/model/arguments.py +43 -0
  38. adagio_cli-0.1.0/src/adagio/model/ast.py +67 -0
  39. adagio_cli-0.1.0/src/adagio/model/pipeline.py +85 -0
  40. adagio_cli-0.1.0/src/adagio/model/task.py +149 -0
  41. adagio_cli-0.1.0/src/adagio/monitor/__init__.py +0 -0
  42. adagio_cli-0.1.0/src/adagio/monitor/api.py +102 -0
  43. adagio_cli-0.1.0/src/adagio/monitor/base.py +0 -0
  44. adagio_cli-0.1.0/src/adagio/monitor/composite.py +99 -0
  45. adagio_cli-0.1.0/src/adagio/monitor/connected.py +212 -0
  46. adagio_cli-0.1.0/src/adagio/monitor/log.py +83 -0
  47. adagio_cli-0.1.0/src/adagio/monitor/tty.py +301 -0
  48. adagio_cli-0.1.0/src/adagio/protocol/__init__.py +0 -0
  49. adagio_cli-0.1.0/src/adagio/protocol/base.py +0 -0
  50. adagio_cli-0.1.0/src/adagio/protocol/file.py +0 -0
  51. adagio_cli-0.1.0/src/adagio/protocol/http.py +0 -0
  52. adagio_cli-0.1.0/src/adagio/protocol/s3.py +0 -0
  53. adagio_cli-0.1.0/src/adagio/qapi/__init__.py +13 -0
  54. adagio_cli-0.1.0/src/adagio/qapi/build.py +418 -0
  55. adagio_cli-0.1.0/src/adagio/qapi/client.py +56 -0
  56. adagio_cli-0.1.0/src/adagio/type_format.py +149 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cymis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: adagio-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line runner for Adagio pipeline files.
5
+ Keywords: adagio,bioinformatics,cli,pipelines,qiime2,workflow
6
+ Author: Cymis
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Dist: cyclopts>=4.5.3
22
+ Requires-Dist: pydantic>=2.12.5
23
+ Requires-Dist: rich>=14.1.0
24
+ Requires-Dist: tomli>=2.2.1 ; python_full_version < '3.11'
25
+ Requires-Python: >=3.10
26
+ Project-URL: Changelog, https://github.com/cymis/adagio-cli/blob/dev/CHANGELOG.md
27
+ Project-URL: Homepage, https://github.com/cymis/adagio-cli
28
+ Project-URL: Issues, https://github.com/cymis/adagio-cli/issues
29
+ Project-URL: Repository, https://github.com/cymis/adagio-cli
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Adagio CLI
33
+
34
+ `adagio-cli` is the Python command-line interface for Adagio pipeline execution.
35
+
36
+ For user-facing documentation and product guides, please reference the docs:
37
+
38
+ - [Adagio Docs](https://docs.adagiodata.com)
39
+
40
+ The adagio frontend is used to build pipelines that can be run with this package on the command line
41
+ It can be found here:
42
+
43
+ - [Adagio](https://adagio.run)
44
+
45
+ ## Development
46
+
47
+ Set up the project and run the test suite with:
48
+
49
+ ```bash
50
+ uv sync --group dev
51
+ uv run pytest
52
+ ```
53
+
54
+ ## Runtime environments
55
+
56
+ Every plugin action requires an explicit execution environment. A runtime config
57
+ passed with `--config` can define one default and override it per plugin or task;
58
+ the CLI never guesses an image from a plugin name.
59
+
60
+ Conda environments are supported with `kind = "conda"`:
61
+
62
+ ```toml
63
+ version = 1
64
+
65
+ [defaults]
66
+ kind = "conda"
67
+ prefix = "/opt/conda/envs/qiime2-2026.1"
68
+
69
+ [plugins]
70
+ dada2 = { kind = "conda", prefix = "/opt/conda/envs/q2-dada2" }
71
+ ```
72
+
73
+ The environment must already exist and contain QIIME 2 plus the plugins needed
74
+ by the pipeline. Adagio enters it with `conda run`; it does not create or manage
75
+ the environment.
76
+
77
+ ## Task resource requests
78
+
79
+ A runtime config can also declare the requested shape of each task execution:
80
+
81
+ ```toml
82
+ [resources.tasks."denoise-node"]
83
+ cpus = 4
84
+ memory = "8 GiB"
85
+ ```
86
+
87
+ `cpus` is a positive whole number and `memory` is a positive, unit-bearing
88
+ quantity. Each entry describes one independently schedulable execution of that
89
+ task, not an aggregate pipeline allocation. If a task is later expanded into
90
+ parallel subtasks, each subtask requests this shape and the scheduler determines
91
+ the total concurrent allocation.
92
+
93
+ The current serial executor parses and validates these fields but intentionally
94
+ does not apply them yet. Omitting either field leaves that resource at the
95
+ executor's default.
96
+
97
+ ## Plugin submission defaults
98
+
99
+ QAPI submissions can persist the environment that Adagio should use for the
100
+ submitted plugins:
101
+
102
+ ```bash
103
+ adagio qapi build --plugin my-plugin \
104
+ --default-conda-prefix /opt/conda/envs/my-plugin
105
+
106
+ adagio qapi build --plugin my-plugin \
107
+ --default-docker-image registry.example.org/my-plugin:2026.1
108
+ ```
109
+
110
+ These options are explicit and mutually exclusive. Omitting both leaves the
111
+ plugin without a default; the Adagio app will flag that plugin until an
112
+ environment is selected for a run.
113
+
114
+ ## Catalog pipelines
115
+
116
+ Run a pipeline from the Adagio pipeline catalog:
117
+
118
+ ```bash
119
+ adagio pipeline show @adagio/microbial-diversity
120
+ adagio run @adagio/microbial-diversity --cache-dir /path/to/cache --arguments run-arguments.json
121
+ ```
122
+
123
+ `@adagio/<slug>` first resolves against a nearby local `adagio-pipelines`
124
+ checkout when one is available. If no local catalog is found, Adagio fetches
125
+ `pipeline.adg` from `cymis/adagio-pipelines` on GitHub, checking `official`
126
+ before `community`.
127
+
128
+ During `adagio run`, remote catalog pipelines are downloaded under the selected
129
+ `--cache-dir` and reused by source name and slug on later runs. `adagio pipeline
130
+ show` uses a temporary download when it fetches from GitHub because it does not
131
+ take a cache directory.
132
+
133
+ Private GitHub access is explicit: set `GITHUB_TOKEN` or `GH_TOKEN` to a token
134
+ that can read `cymis/adagio-pipelines`; with a token, the CLI fetches through
135
+ the GitHub contents API. The CLI does not read browser, git, or `gh` credentials
136
+ automatically.
@@ -0,0 +1,105 @@
1
+ # Adagio CLI
2
+
3
+ `adagio-cli` is the Python command-line interface for Adagio pipeline execution.
4
+
5
+ For user-facing documentation and product guides, please reference the docs:
6
+
7
+ - [Adagio Docs](https://docs.adagiodata.com)
8
+
9
+ The adagio frontend is used to build pipelines that can be run with this package on the command line
10
+ It can be found here:
11
+
12
+ - [Adagio](https://adagio.run)
13
+
14
+ ## Development
15
+
16
+ Set up the project and run the test suite with:
17
+
18
+ ```bash
19
+ uv sync --group dev
20
+ uv run pytest
21
+ ```
22
+
23
+ ## Runtime environments
24
+
25
+ Every plugin action requires an explicit execution environment. A runtime config
26
+ passed with `--config` can define one default and override it per plugin or task;
27
+ the CLI never guesses an image from a plugin name.
28
+
29
+ Conda environments are supported with `kind = "conda"`:
30
+
31
+ ```toml
32
+ version = 1
33
+
34
+ [defaults]
35
+ kind = "conda"
36
+ prefix = "/opt/conda/envs/qiime2-2026.1"
37
+
38
+ [plugins]
39
+ dada2 = { kind = "conda", prefix = "/opt/conda/envs/q2-dada2" }
40
+ ```
41
+
42
+ The environment must already exist and contain QIIME 2 plus the plugins needed
43
+ by the pipeline. Adagio enters it with `conda run`; it does not create or manage
44
+ the environment.
45
+
46
+ ## Task resource requests
47
+
48
+ A runtime config can also declare the requested shape of each task execution:
49
+
50
+ ```toml
51
+ [resources.tasks."denoise-node"]
52
+ cpus = 4
53
+ memory = "8 GiB"
54
+ ```
55
+
56
+ `cpus` is a positive whole number and `memory` is a positive, unit-bearing
57
+ quantity. Each entry describes one independently schedulable execution of that
58
+ task, not an aggregate pipeline allocation. If a task is later expanded into
59
+ parallel subtasks, each subtask requests this shape and the scheduler determines
60
+ the total concurrent allocation.
61
+
62
+ The current serial executor parses and validates these fields but intentionally
63
+ does not apply them yet. Omitting either field leaves that resource at the
64
+ executor's default.
65
+
66
+ ## Plugin submission defaults
67
+
68
+ QAPI submissions can persist the environment that Adagio should use for the
69
+ submitted plugins:
70
+
71
+ ```bash
72
+ adagio qapi build --plugin my-plugin \
73
+ --default-conda-prefix /opt/conda/envs/my-plugin
74
+
75
+ adagio qapi build --plugin my-plugin \
76
+ --default-docker-image registry.example.org/my-plugin:2026.1
77
+ ```
78
+
79
+ These options are explicit and mutually exclusive. Omitting both leaves the
80
+ plugin without a default; the Adagio app will flag that plugin until an
81
+ environment is selected for a run.
82
+
83
+ ## Catalog pipelines
84
+
85
+ Run a pipeline from the Adagio pipeline catalog:
86
+
87
+ ```bash
88
+ adagio pipeline show @adagio/microbial-diversity
89
+ adagio run @adagio/microbial-diversity --cache-dir /path/to/cache --arguments run-arguments.json
90
+ ```
91
+
92
+ `@adagio/<slug>` first resolves against a nearby local `adagio-pipelines`
93
+ checkout when one is available. If no local catalog is found, Adagio fetches
94
+ `pipeline.adg` from `cymis/adagio-pipelines` on GitHub, checking `official`
95
+ before `community`.
96
+
97
+ During `adagio run`, remote catalog pipelines are downloaded under the selected
98
+ `--cache-dir` and reused by source name and slug on later runs. `adagio pipeline
99
+ show` uses a temporary download when it fetches from GitHub because it does not
100
+ take a cache directory.
101
+
102
+ Private GitHub access is explicit: set `GITHUB_TOKEN` or `GH_TOKEN` to a token
103
+ that can read `cymis/adagio-pipelines`; with a token, the CLI fetches through
104
+ the GitHub contents API. The CLI does not read browser, git, or `gh` credentials
105
+ automatically.
@@ -0,0 +1,91 @@
1
+ [project]
2
+ name = "adagio-cli"
3
+ version = "0.1.0"
4
+ description = "Command-line runner for Adagio pipeline files."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ authors = [{ name = "Cymis" }]
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ keywords = [
11
+ "adagio",
12
+ "bioinformatics",
13
+ "cli",
14
+ "pipelines",
15
+ "qiime2",
16
+ "workflow",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Developers",
22
+ "Intended Audience :: Science/Research",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
32
+ dependencies = [
33
+ "cyclopts>=4.5.3",
34
+ "pydantic>=2.12.5",
35
+ "rich>=14.1.0",
36
+ "tomli>=2.2.1; python_version < '3.11'",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/cymis/adagio-cli"
41
+ Repository = "https://github.com/cymis/adagio-cli"
42
+ Issues = "https://github.com/cymis/adagio-cli/issues"
43
+ Changelog = "https://github.com/cymis/adagio-cli/blob/dev/CHANGELOG.md"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "build>=1.2.2",
48
+ "nox>=2024.4.15,<2025.0.0",
49
+ "pytest>=8.4.0",
50
+ "pytest-cov>=6.0.0",
51
+ "ruff>=0.13.0",
52
+ "twine>=6.2.0",
53
+ ]
54
+
55
+ [project.scripts]
56
+ adagio = "adagio.cli.main:main"
57
+
58
+ [build-system]
59
+ requires = ["uv_build>=0.8.17,<0.9.0"]
60
+ build-backend = "uv_build"
61
+
62
+ [tool.uv.build-backend]
63
+ module-name = "adagio"
64
+
65
+ [tool.ruff.format]
66
+ quote-style = "double"
67
+ indent-style = "space"
68
+ docstring-code-format = true
69
+
70
+ [tool.ruff.lint.isort]
71
+
72
+
73
+ known-first-party = ["adagio"]
74
+ section-order = [
75
+ "future",
76
+ "standard-library",
77
+ "third-party",
78
+ "first-party",
79
+ "local-folder",
80
+ ]
81
+ [tool.mypy]
82
+ plugins = ['pydantic.mypy']
83
+
84
+ [[tool.mypy.overrides]]
85
+
86
+ module = ["qiime2.*"]
87
+ ignore_missing_imports = true
88
+
89
+ [tool.pytest.ini_options]
90
+ addopts = "-ra"
91
+ testpaths = ["tests"]
@@ -0,0 +1,15 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+
4
+ def _resolve_version() -> str:
5
+ for dist_name in ("adagio-cli", "adagio"):
6
+ try:
7
+ return version(dist_name)
8
+ except PackageNotFoundError:
9
+ continue
10
+ return "0.0.0"
11
+
12
+
13
+ __version__ = _resolve_version()
14
+
15
+ __all__ = ["__version__"]
@@ -0,0 +1,89 @@
1
+ """Helpers for pulling promoted parameter specs from pipeline JSON."""
2
+
3
+ from typing import Any, List, Optional
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class Parameter(BaseModel):
10
+ id: UUID
11
+ name: str
12
+ required: bool
13
+ default: Optional[Any] = None
14
+ type: str
15
+ description: Optional[str] = None
16
+
17
+
18
+ class Input(BaseModel):
19
+ id: UUID
20
+ name: str
21
+ required: bool
22
+ type: str
23
+ description: Optional[str] = None
24
+
25
+
26
+ class Output(BaseModel):
27
+ id: UUID
28
+ name: str
29
+ type: str
30
+ description: Optional[str] = None
31
+
32
+
33
+ def _extract_signature(data: Any) -> dict[str, Any]:
34
+ signature = (
35
+ data.get("spec", {}).get("signature")
36
+ if isinstance(data, dict)
37
+ else None
38
+ ) or (data.get("signature") if isinstance(data, dict) else None)
39
+
40
+ if not isinstance(signature, dict):
41
+ raise ValueError(
42
+ "Invalid pipeline: missing 'signature' section in pipeline JSON."
43
+ )
44
+
45
+ return signature
46
+
47
+
48
+ def parse_parameters(data: Any) -> List[Parameter]:
49
+ """Parse promoted parameters from supported pipeline JSON layouts.
50
+
51
+ We currently accept either:
52
+ - {"spec": {"signature": {"parameters": [...]}}}
53
+ - {"signature": {"parameters": [...]} }
54
+ """
55
+ signature = _extract_signature(data)
56
+
57
+ raw_parameters = signature.get("parameters")
58
+ if not isinstance(raw_parameters, list):
59
+ raise ValueError(
60
+ "Invalid pipeline: missing 'signature.parameters' list in pipeline JSON."
61
+ )
62
+
63
+ return [Parameter(**param) for param in raw_parameters]
64
+
65
+
66
+ def parse_inputs(data: Any) -> List[Input]:
67
+ """Parse promoted inputs from supported pipeline JSON layouts."""
68
+ signature = _extract_signature(data)
69
+
70
+ raw_inputs = signature.get("inputs")
71
+ if not isinstance(raw_inputs, list):
72
+ raise ValueError(
73
+ "Invalid pipeline: missing 'signature.inputs' list in pipeline JSON."
74
+ )
75
+
76
+ return [Input(**input_item) for input_item in raw_inputs]
77
+
78
+
79
+ def parse_outputs(data: Any) -> List[Output]:
80
+ """Parse pipeline outputs from supported pipeline JSON layouts."""
81
+ signature = _extract_signature(data)
82
+
83
+ raw_outputs = signature.get("outputs")
84
+ if not isinstance(raw_outputs, list):
85
+ raise ValueError(
86
+ "Invalid pipeline: missing 'signature.outputs' list in pipeline JSON."
87
+ )
88
+
89
+ return [Output(**output_item) for output_item in raw_outputs]
@@ -0,0 +1,16 @@
1
+
2
+
3
+ def check_dependencies(spec):
4
+ pass
5
+
6
+
7
+ def check_version(spec):
8
+ pass
9
+
10
+
11
+ def check_graph(spec):
12
+ pass
13
+
14
+
15
+ def check_arguments(spec, arguments):
16
+ pass
@@ -0,0 +1 @@
1
+ __all__: list[str] = []
@@ -0,0 +1,62 @@
1
+ import re
2
+ from enum import Enum
3
+
4
+ try:
5
+ from enum import StrEnum
6
+ except ImportError: # pragma: no cover - Python < 3.11
7
+ class StrEnum(str, Enum):
8
+ pass
9
+
10
+
11
+ class ParamType(StrEnum):
12
+ INPUT = "input"
13
+ PARAM = "param"
14
+ OUTPUT = "output"
15
+
16
+
17
+ class ShowParamsMode(StrEnum):
18
+ ALL = "all"
19
+ MISSING = "missing"
20
+ REQUIRED = "required"
21
+
22
+
23
+ def promote_positional_pipeline(argv: list[str]) -> tuple[list[str], str | None]:
24
+ """Allow `adagio run <pipeline>` by rewriting it to `--pipeline <pipeline>`."""
25
+ if len(argv) < 2 or argv[0] != "run":
26
+ return argv, None
27
+
28
+ candidate = argv[1]
29
+ if candidate.startswith("-"):
30
+ return argv, None
31
+
32
+ rewritten = ["run", "--pipeline", candidate, *argv[2:]]
33
+ return rewritten, candidate
34
+
35
+
36
+ def extract_flag_value(argv: list[str], *flags: str) -> str | None:
37
+ """Supports: --flag value, -f value, --flag=value."""
38
+ flag_set = set(flags)
39
+ for i, tok in enumerate(argv):
40
+ if tok in flag_set:
41
+ return argv[i + 1] if i + 1 < len(argv) else None
42
+ for flag in flags:
43
+ if tok.startswith(flag + "="):
44
+ return tok.split("=", 1)[1]
45
+ return None
46
+
47
+
48
+ def to_identifier(name: str, prefix: str | None = None) -> str:
49
+ """Turn arbitrary names into valid Python identifiers for kwargs keys."""
50
+ clean = (name or "").strip()
51
+ clean = re.sub(r"[^0-9a-zA-Z_]+", "_", clean)
52
+ if not clean:
53
+ raise ValueError("Empty parameter name in pipeline file.")
54
+ if clean[0].isdigit():
55
+ clean = "_" + clean
56
+ if prefix:
57
+ return f"{prefix}_{clean}"
58
+ return clean
59
+
60
+
61
+ def dynamic_opt(name: str, param_type: ParamType) -> str:
62
+ return f"--{param_type}-{name.replace('_', '-')}"
@@ -0,0 +1,82 @@
1
+ import re
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ from cyclopts import App, Group, Parameter
7
+ from rich.console import Console
8
+
9
+ from ..executors.cache_support import CACHE_DIR_HELP, resolve_cache_dir_path
10
+
11
+ QIIME_CACHE_CONTENTS = {"VERSION", "data", "keys", "pools", "processes"}
12
+ QIIME_CACHE_LINE_RE = re.compile(r"cache: v?\d+\Z")
13
+
14
+
15
+ def run_cache(argv: list[str], *, console: Console) -> None:
16
+ app = App(
17
+ name="adagio cache",
18
+ help="Manage Adagio's shared QIIME cache directory.",
19
+ )
20
+ command_group = Group("Command Options", sort_key=0)
21
+
22
+ @app.command
23
+ def clear(
24
+ *,
25
+ cache_dir: Annotated[
26
+ Path,
27
+ Parameter(
28
+ name=("--cache-dir",),
29
+ group=command_group,
30
+ help=CACHE_DIR_HELP,
31
+ ),
32
+ ],
33
+ ) -> None:
34
+ """Delete an existing QIIME cache directory."""
35
+ resolved_cache_dir = resolve_cache_dir_path(
36
+ cwd=Path.cwd().resolve(),
37
+ raw_value=str(cache_dir),
38
+ )
39
+ _clear_cache(cache_dir=resolved_cache_dir, console=console)
40
+
41
+ app(argv)
42
+
43
+
44
+ def _clear_cache(*, cache_dir: Path, console: Console) -> None:
45
+ _require_qiime_cache(cache_dir)
46
+ shutil.rmtree(cache_dir)
47
+ console.print(f"Cleared cache directory: {cache_dir}")
48
+
49
+
50
+ def _require_qiime_cache(cache_dir: Path) -> None:
51
+ if not cache_dir.exists():
52
+ raise SystemExit(f"Cache directory does not exist: {cache_dir}")
53
+ if not cache_dir.is_dir():
54
+ raise SystemExit(f"Cache path is not a directory: {cache_dir}")
55
+
56
+ contents = set(item.name for item in cache_dir.iterdir())
57
+ if not contents.issuperset(QIIME_CACHE_CONTENTS):
58
+ raise SystemExit(f"Path is not a QIIME cache: {cache_dir}")
59
+
60
+ version_file = cache_dir / "VERSION"
61
+ try:
62
+ version_text = version_file.read_text(encoding="utf-8").strip()
63
+ except OSError as exc:
64
+ raise SystemExit(f"Could not read cache version file: {version_file}") from exc
65
+
66
+ if not _looks_like_qiime_cache_version(version_text):
67
+ raise SystemExit(f"Path is not a QIIME cache: {cache_dir}")
68
+
69
+
70
+ def _looks_like_qiime_cache_version(version_text: str) -> bool:
71
+ lines = version_text.splitlines()
72
+ if len(lines) != 3:
73
+ return False
74
+
75
+ if lines[0] != "QIIME 2":
76
+ return False
77
+
78
+ if not QIIME_CACHE_LINE_RE.fullmatch(lines[1]):
79
+ return False
80
+
81
+ framework_prefix = "framework: "
82
+ return lines[2].startswith(framework_prefix) and bool(lines[2][len(framework_prefix) :].strip())