adagio-cli 0.1.0__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 +183 -0
- adagio/cli/dynamic.py +656 -0
- adagio/cli/main.py +313 -0
- adagio/cli/pipeline.py +49 -0
- adagio/cli/pipeline_sources.py +322 -0
- adagio/cli/qapi.py +300 -0
- adagio/cli/runner.py +310 -0
- adagio/cli/runtime.py +530 -0
- adagio/cli/task_exec.py +474 -0
- adagio/convert.py +0 -0
- adagio/describe.py +487 -0
- adagio/executors/__init__.py +34 -0
- adagio/executors/apptainer.py +275 -0
- adagio/executors/base.py +92 -0
- adagio/executors/cache_support.py +82 -0
- adagio/executors/common.py +124 -0
- adagio/executors/conda.py +314 -0
- adagio/executors/container_support.py +260 -0
- adagio/executors/defaults.py +99 -0
- adagio/executors/docker.py +404 -0
- adagio/executors/path_utils.py +60 -0
- adagio/executors/serial_runner.py +318 -0
- adagio/executors/signature.py +140 -0
- adagio/executors/task_contract.py +125 -0
- adagio/executors/task_environments.py +760 -0
- adagio/model/__init__.py +0 -0
- adagio/model/arguments.py +43 -0
- adagio/model/ast.py +67 -0
- adagio/model/pipeline.py +85 -0
- adagio/model/task.py +149 -0
- adagio/monitor/__init__.py +0 -0
- adagio/monitor/api.py +102 -0
- adagio/monitor/base.py +0 -0
- adagio/monitor/composite.py +99 -0
- adagio/monitor/connected.py +212 -0
- adagio/monitor/log.py +83 -0
- adagio/monitor/tty.py +301 -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 +13 -0
- adagio/qapi/build.py +418 -0
- adagio/qapi/client.py +56 -0
- adagio/type_format.py +149 -0
- adagio_cli-0.1.0.dist-info/METADATA +136 -0
- adagio_cli-0.1.0.dist-info/RECORD +57 -0
- adagio_cli-0.1.0.dist-info/WHEEL +4 -0
- adagio_cli-0.1.0.dist-info/entry_points.txt +3 -0
- adagio_cli-0.1.0.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,183 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from decimal import Decimal, InvalidOperation
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from ..executors.base import TaskEnvironmentOverride
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import tomllib
|
|
13
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
14
|
+
import tomli as tomllib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnvironmentOverride(BaseModel):
|
|
18
|
+
model_config = ConfigDict(extra="forbid")
|
|
19
|
+
|
|
20
|
+
kind: str | None = None
|
|
21
|
+
image: str | None = None
|
|
22
|
+
prefix: str | None = None
|
|
23
|
+
platform: str | None = None
|
|
24
|
+
conda_executable: str | None = None
|
|
25
|
+
options: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
@model_validator(mode="after")
|
|
28
|
+
def _validate_reference_fields(self) -> "EnvironmentOverride":
|
|
29
|
+
configured = [
|
|
30
|
+
name
|
|
31
|
+
for name, value in (
|
|
32
|
+
("image", self.image),
|
|
33
|
+
("prefix", self.prefix),
|
|
34
|
+
)
|
|
35
|
+
if value is not None
|
|
36
|
+
]
|
|
37
|
+
if len(configured) > 1:
|
|
38
|
+
names = ", ".join(configured)
|
|
39
|
+
raise ValueError(f"Only one environment reference field may be set: {names}")
|
|
40
|
+
# Stripped BEFORE the absoluteness check and stored stripped, matching
|
|
41
|
+
# the UI's trimmed validation and the backend validator - otherwise
|
|
42
|
+
# " /opt/env" is rejected after the UI called it valid, and
|
|
43
|
+
# "/opt/env " names a different directory with a trailing space.
|
|
44
|
+
# Absoluteness is checked before ``.resolve()`` so a relative spelling
|
|
45
|
+
# fails loudly instead of silently binding to the CLI's working
|
|
46
|
+
# directory. ``~`` is fine - expanduser() yields an absolute path.
|
|
47
|
+
if self.prefix is not None:
|
|
48
|
+
self.prefix = self.prefix.strip()
|
|
49
|
+
if not Path(self.prefix).expanduser().is_absolute():
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f'Conda prefix must be an absolute path; got "{self.prefix}".'
|
|
52
|
+
)
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def to_task_environment_override(self) -> TaskEnvironmentOverride | None:
|
|
56
|
+
options = dict(self.options)
|
|
57
|
+
reference = self.image
|
|
58
|
+
if self.prefix is not None:
|
|
59
|
+
reference = str(Path(self.prefix).expanduser().resolve())
|
|
60
|
+
if self.conda_executable is not None:
|
|
61
|
+
options["conda_executable"] = self.conda_executable
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
self.kind is None
|
|
65
|
+
and reference is None
|
|
66
|
+
and self.platform is None
|
|
67
|
+
and not options
|
|
68
|
+
):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
return TaskEnvironmentOverride(
|
|
72
|
+
kind=self.kind,
|
|
73
|
+
reference=reference,
|
|
74
|
+
platform=self.platform,
|
|
75
|
+
options=options or None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_MEMORY_REQUEST_PATTERN = re.compile(
|
|
80
|
+
r"^(?P<amount>(?:\d+(?:\.\d+)?|\.\d+))\s*"
|
|
81
|
+
r"(?:B|KB|MB|GB|TB|PB|KiB|MiB|GiB|TiB|PiB)$",
|
|
82
|
+
re.IGNORECASE,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TaskResourceRequirements(BaseModel):
|
|
87
|
+
"""Requested shape for one task execution.
|
|
88
|
+
|
|
89
|
+
These values are parsed and retained for forward compatibility. The serial
|
|
90
|
+
executor intentionally does not apply them yet.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
model_config = ConfigDict(extra="forbid")
|
|
94
|
+
|
|
95
|
+
cpus: int | None = Field(default=None, ge=1, strict=True)
|
|
96
|
+
memory: str | None = None
|
|
97
|
+
|
|
98
|
+
@field_validator("memory")
|
|
99
|
+
@classmethod
|
|
100
|
+
def _validate_memory(cls, value: str | None) -> str | None:
|
|
101
|
+
if value is None:
|
|
102
|
+
return None
|
|
103
|
+
normalized = value.strip()
|
|
104
|
+
match = _MEMORY_REQUEST_PATTERN.fullmatch(normalized)
|
|
105
|
+
if match is None:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
'Memory must be a positive, unit-bearing quantity such as "8 GiB".'
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
amount = Decimal(match.group("amount"))
|
|
111
|
+
except InvalidOperation as err: # pragma: no cover - guarded by regex
|
|
112
|
+
raise ValueError("Memory amount is invalid.") from err
|
|
113
|
+
if amount <= 0:
|
|
114
|
+
raise ValueError("Memory must be greater than zero.")
|
|
115
|
+
return normalized
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ResourceRequirementsConfig(BaseModel):
|
|
119
|
+
model_config = ConfigDict(extra="forbid")
|
|
120
|
+
|
|
121
|
+
tasks: dict[str, TaskResourceRequirements] = Field(default_factory=dict)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AdagioRunConfig(BaseModel):
|
|
125
|
+
version: int = 1
|
|
126
|
+
defaults: EnvironmentOverride = Field(default_factory=EnvironmentOverride)
|
|
127
|
+
plugins: dict[str, EnvironmentOverride] = Field(default_factory=dict)
|
|
128
|
+
tasks: dict[str, EnvironmentOverride] = Field(default_factory=dict)
|
|
129
|
+
resources: ResourceRequirementsConfig = Field(
|
|
130
|
+
default_factory=ResourceRequirementsConfig
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_run_config(path: Path | None) -> AdagioRunConfig | None:
|
|
135
|
+
if path is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
data = _parse_config_text(path.read_text(encoding="utf-8"))
|
|
139
|
+
if not isinstance(data, dict):
|
|
140
|
+
raise SystemExit("Invalid config file: expected a TOML table or JSON object.")
|
|
141
|
+
|
|
142
|
+
return AdagioRunConfig.model_validate(data)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _parse_config_text(text: str) -> Any:
|
|
146
|
+
"""Parse a runtime config as TOML *or* JSON (auto-detect).
|
|
147
|
+
|
|
148
|
+
The runtime launch contract lets an adapter hand the CLI a valid
|
|
149
|
+
``AdagioRunConfig`` serialized as either TOML or JSON (design §5.7). A JSON
|
|
150
|
+
document (leading ``{``/``[``) parses cleanly as JSON but not as TOML, so we
|
|
151
|
+
sniff the first non-whitespace character and prefer JSON there; otherwise we
|
|
152
|
+
parse TOML. Existing TOML behavior is unchanged — a TOML config never starts
|
|
153
|
+
with ``{``/``[`` at document scope (``[table]`` headers do, so we fall back
|
|
154
|
+
to TOML on JSON-parse failure to stay safe).
|
|
155
|
+
"""
|
|
156
|
+
stripped = text.lstrip()
|
|
157
|
+
if stripped[:1] in ("{", "["):
|
|
158
|
+
try:
|
|
159
|
+
return json.loads(text)
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
# A TOML file legitimately begins with an ``[table]`` header; fall
|
|
162
|
+
# through to the TOML parser rather than failing outright.
|
|
163
|
+
pass
|
|
164
|
+
return tomllib.loads(text)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def default_environment_override(
|
|
168
|
+
run_config: AdagioRunConfig | None,
|
|
169
|
+
) -> TaskEnvironmentOverride | None:
|
|
170
|
+
if run_config is None:
|
|
171
|
+
return None
|
|
172
|
+
return run_config.defaults.to_task_environment_override()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def named_environment_overrides(
|
|
176
|
+
raw_overrides: dict[str, EnvironmentOverride],
|
|
177
|
+
) -> dict[str, TaskEnvironmentOverride] | None:
|
|
178
|
+
resolved = {
|
|
179
|
+
name: override
|
|
180
|
+
for name, raw_override in raw_overrides.items()
|
|
181
|
+
if (override := raw_override.to_task_environment_override()) is not None
|
|
182
|
+
}
|
|
183
|
+
return resolved or None
|