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.
- adagio/__init__.py +15 -0
- adagio/app/parsers/pipeline.py +89 -0
- adagio/check.py +16 -0
- adagio/cli/__init__.py +1 -0
- adagio/cli/args.py +62 -0
- adagio/cli/cache.py +82 -0
- adagio/cli/config.py +39 -0
- adagio/cli/dynamic.py +618 -0
- adagio/cli/main.py +263 -0
- adagio/cli/pipeline.py +27 -0
- adagio/cli/qapi.py +172 -0
- adagio/cli/runner.py +285 -0
- adagio/cli/runtime.py +388 -0
- adagio/cli/task_exec.py +289 -0
- adagio/convert.py +0 -0
- adagio/describe.py +366 -0
- adagio/execute.py +65 -0
- adagio/execution/context.py +102 -0
- adagio/execution/proxy.py +243 -0
- adagio/executors/__init__.py +29 -0
- adagio/executors/apptainer.py +214 -0
- adagio/executors/base.py +78 -0
- adagio/executors/cache_support.py +55 -0
- adagio/executors/common.py +55 -0
- adagio/executors/container_support.py +169 -0
- adagio/executors/defaults.py +115 -0
- adagio/executors/docker.py +184 -0
- adagio/executors/path_utils.py +47 -0
- adagio/executors/serial_runner.py +135 -0
- adagio/executors/task_contract.py +88 -0
- adagio/executors/task_environments.py +225 -0
- adagio/io.py +41 -0
- adagio/model/__init__.py +0 -0
- adagio/model/arguments.py +39 -0
- adagio/model/ast.py +55 -0
- adagio/model/pipeline.py +134 -0
- adagio/model/task.py +135 -0
- adagio/monitor/__init__.py +0 -0
- adagio/monitor/api.py +60 -0
- adagio/monitor/base.py +0 -0
- adagio/monitor/composite.py +74 -0
- adagio/monitor/connected.py +106 -0
- adagio/monitor/log.py +72 -0
- adagio/monitor/tty.py +293 -0
- adagio/protocol/__init__.py +0 -0
- adagio/protocol/base.py +0 -0
- adagio/protocol/file.py +0 -0
- adagio/protocol/http.py +0 -0
- adagio/protocol/s3.py +0 -0
- adagio/qapi/__init__.py +8 -0
- adagio/qapi/build.py +158 -0
- adagio/qapi/client.py +56 -0
- adagio_cli-0.1.0a1.dist-info/METADATA +310 -0
- adagio_cli-0.1.0a1.dist-info/RECORD +57 -0
- adagio_cli-0.1.0a1.dist-info/WHEEL +4 -0
- adagio_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
- 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
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)
|