adagio-cli 0.1.0a1__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.
Files changed (57) hide show
  1. adagio/__init__.py +15 -0
  2. adagio/app/parsers/pipeline.py +89 -0
  3. adagio/check.py +16 -0
  4. adagio/cli/__init__.py +1 -0
  5. adagio/cli/args.py +62 -0
  6. adagio/cli/cache.py +82 -0
  7. adagio/cli/config.py +39 -0
  8. adagio/cli/dynamic.py +618 -0
  9. adagio/cli/main.py +263 -0
  10. adagio/cli/pipeline.py +27 -0
  11. adagio/cli/qapi.py +172 -0
  12. adagio/cli/runner.py +285 -0
  13. adagio/cli/runtime.py +388 -0
  14. adagio/cli/task_exec.py +289 -0
  15. adagio/convert.py +0 -0
  16. adagio/describe.py +366 -0
  17. adagio/execute.py +65 -0
  18. adagio/execution/context.py +102 -0
  19. adagio/execution/proxy.py +243 -0
  20. adagio/executors/__init__.py +29 -0
  21. adagio/executors/apptainer.py +214 -0
  22. adagio/executors/base.py +78 -0
  23. adagio/executors/cache_support.py +55 -0
  24. adagio/executors/common.py +55 -0
  25. adagio/executors/container_support.py +169 -0
  26. adagio/executors/defaults.py +115 -0
  27. adagio/executors/docker.py +184 -0
  28. adagio/executors/path_utils.py +47 -0
  29. adagio/executors/serial_runner.py +135 -0
  30. adagio/executors/task_contract.py +88 -0
  31. adagio/executors/task_environments.py +225 -0
  32. adagio/io.py +41 -0
  33. adagio/model/__init__.py +0 -0
  34. adagio/model/arguments.py +39 -0
  35. adagio/model/ast.py +55 -0
  36. adagio/model/pipeline.py +134 -0
  37. adagio/model/task.py +135 -0
  38. adagio/monitor/__init__.py +0 -0
  39. adagio/monitor/api.py +60 -0
  40. adagio/monitor/base.py +0 -0
  41. adagio/monitor/composite.py +74 -0
  42. adagio/monitor/connected.py +106 -0
  43. adagio/monitor/log.py +72 -0
  44. adagio/monitor/tty.py +293 -0
  45. adagio/protocol/__init__.py +0 -0
  46. adagio/protocol/base.py +0 -0
  47. adagio/protocol/file.py +0 -0
  48. adagio/protocol/http.py +0 -0
  49. adagio/protocol/s3.py +0 -0
  50. adagio/qapi/__init__.py +8 -0
  51. adagio/qapi/build.py +158 -0
  52. adagio/qapi/client.py +56 -0
  53. adagio_cli-0.1.0a1.dist-info/METADATA +310 -0
  54. adagio_cli-0.1.0a1.dist-info/RECORD +57 -0
  55. adagio_cli-0.1.0a1.dist-info/WHEEL +4 -0
  56. adagio_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
  57. adagio_cli-0.1.0a1.dist-info/licenses/LICENSE +21 -0
adagio/__init__.py ADDED
@@ -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]
adagio/check.py ADDED
@@ -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
adagio/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __all__: list[str] = []
adagio/cli/args.py ADDED
@@ -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('_', '-')}"
adagio/cli/cache.py ADDED
@@ -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())
adagio/cli/config.py ADDED
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ try:
7
+ import tomllib
8
+ except ModuleNotFoundError: # pragma: no cover
9
+ import tomli as tomllib
10
+
11
+
12
+ class ImageOverride(BaseModel):
13
+ kind: Literal["docker", "apptainer"] | None = None
14
+ image: str | None = None
15
+ platform: str | None = None
16
+
17
+
18
+ class DefaultOverride(BaseModel):
19
+ kind: Literal["docker", "apptainer"] | None = None
20
+ image: str | None = None
21
+ platform: str | None = None
22
+
23
+
24
+ class AdagioRunConfig(BaseModel):
25
+ version: int = 1
26
+ defaults: DefaultOverride = Field(default_factory=DefaultOverride)
27
+ plugins: dict[str, ImageOverride] = Field(default_factory=dict)
28
+ tasks: dict[str, ImageOverride] = Field(default_factory=dict)
29
+
30
+
31
+ def load_run_config(path: Path | None) -> AdagioRunConfig | None:
32
+ if path is None:
33
+ return None
34
+
35
+ data = tomllib.loads(path.read_text(encoding="utf-8"))
36
+ if not isinstance(data, dict):
37
+ raise SystemExit("Invalid config file: expected a TOML table.")
38
+
39
+ return AdagioRunConfig.model_validate(data)