@delta-infra/cli 1.0.76 → 1.0.78
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.
- package/README.md +405 -405
- package/checksums.txt +5 -5
- package/package.json +1 -1
- package/scripts/install-wizard.js +610 -596
- package/scripts/install.js +400 -400
- package/scripts/run.js +65 -65
- package/skills/antbo-ldm-guard/SKILL.md +51 -51
- package/skills/antbo-ldm-guard/agents/openai.yaml +4 -4
- package/skills/antbo-service/SKILL.md +71 -71
- package/skills/antbo-service/agents/openai.yaml +4 -4
- package/skills/antbo-service/references/operations.md +45 -45
- package/skills/delta-bo/SKILL.md +28 -28
- package/skills/delta-bo/agents/openai.yaml +4 -4
- package/skills/delta-bo/references/endpoints.md +263 -263
- package/skills/delta-sandbox/SKILL.md +356 -356
- package/skills/delta-sandbox/agents/openai.yaml +4 -4
- package/skills/delta-sandbox/references/commands.md +56 -56
- package/skills/delta-sandbox/references/lifecycle.md +92 -92
- package/skills/delta-sandbox/references/recipes.md +133 -133
- package/skills/delta-science/SKILL.md +163 -163
- package/skills/delta-science/agents/openai.yaml +4 -4
- package/skills/delta-science/references/cli-contract.md +84 -84
- package/skills/delta-science/references/services.md +112 -112
- package/skills/delta-science/references/workflows.md +44 -44
- package/skills/delta-science/scripts/invoke.py +257 -257
- package/skills/delta-shared/SKILL.md +95 -95
- package/skills/delta-shared/agents/openai.yaml +4 -4
- package/skills/gsasii/SKILL.md +34 -34
- package/skills/gsasii/agents/openai.yaml +4 -4
- package/skills/gsasii/references/endpoints.md +130 -130
- package/skills/lammps/SKILL.md +31 -31
- package/skills/lammps/agents/openai.yaml +4 -4
- package/skills/lammps/references/endpoints.md +61 -61
- package/skills/ldm-bo/SKILL.md +27 -27
- package/skills/ldm-bo/agents/openai.yaml +4 -4
- package/skills/ldm-bo/references/endpoints.md +127 -127
- package/skills/pubchem/SKILL.md +85 -85
- package/skills/pubchem/agents/openai.yaml +4 -4
- package/skills/pubchem/references/endpoints.md +122 -122
- package/skills/pubchem/references/operations.md +41 -41
- package/skills/pymatgen/SKILL.md +46 -46
- package/skills/pymatgen/agents/openai.yaml +4 -4
- package/skills/pymatgen/references/endpoints.md +66 -66
- package/skills/rdkit/SKILL.md +104 -104
- package/skills/rdkit/agents/openai.yaml +4 -4
- package/skills/rdkit/references/endpoints.md +133 -133
- package/skills/rdkit/references/operations.md +64 -64
- package/skills/synbo-service/SKILL.md +28 -28
- package/skills/synbo-service/agents/openai.yaml +4 -4
- package/skills/synbo-service/references/endpoints.md +157 -157
|
@@ -1,263 +1,263 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Invoke one Delta Science operation strictly through delta-cli."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
import shutil
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
import time
|
|
14
|
-
from typing import Any
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Invoke one Delta Science operation strictly through delta-cli."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any
|
|
15
15
|
from urllib.parse import urlsplit
|
|
16
16
|
|
|
17
17
|
from generated_catalog import LEGACY_ENDPOINTS, LEGACY_TOOL_ALIASES
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
SKILL_DIR = Path(__file__).resolve().parents[1]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
|
|
24
|
-
if raw is None:
|
|
25
|
-
return None
|
|
26
|
-
try:
|
|
27
|
-
value = json.loads(raw)
|
|
28
|
-
except json.JSONDecodeError as exc:
|
|
29
|
-
raise ValueError(f"{flag} is not valid JSON: {exc}") from exc
|
|
30
|
-
if not isinstance(value, dict):
|
|
31
|
-
raise ValueError(f"{flag} must decode to a JSON object")
|
|
32
|
-
return value
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _runtime_config() -> dict[str, Any]:
|
|
36
|
-
path = SKILL_DIR / "runtime.local.json"
|
|
37
|
-
if not path.is_file():
|
|
38
|
-
return {}
|
|
39
|
-
try:
|
|
40
|
-
value = json.loads(path.read_text(encoding="utf-8"))
|
|
41
|
-
except (OSError, json.JSONDecodeError):
|
|
42
|
-
return {}
|
|
43
|
-
return value if isinstance(value, dict) else {}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
|
|
47
|
-
candidates: list[str | None] = [
|
|
48
|
-
explicit,
|
|
49
|
-
os.environ.get("DELTA_CLI_PATH"),
|
|
50
|
-
runtime.get("cli_path"),
|
|
51
|
-
shutil.which("delta-cli"),
|
|
52
|
-
shutil.which("delta-cli.exe"),
|
|
53
|
-
]
|
|
54
|
-
package_root = SKILL_DIR.parents[1]
|
|
55
|
-
exe_name = "delta-cli.exe" if os.name == "nt" else "delta-cli"
|
|
56
|
-
candidates.append(str(package_root / "bin" / exe_name))
|
|
57
|
-
for candidate in candidates:
|
|
58
|
-
if candidate and Path(candidate).is_file():
|
|
59
|
-
return str(Path(candidate).resolve())
|
|
60
|
-
raise FileNotFoundError(
|
|
61
|
-
"delta-cli not found; install @delta-infra/cli, set DELTA_CLI_PATH, "
|
|
62
|
-
"or add cli_path to delta-science/runtime.local.json"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _cli_science_base_url(cli: str, env: dict[str, str], timeout: float) -> str:
|
|
67
|
-
explicit = env.get("DELTA_INFRA_SCIENCE_BASE_URL", "").strip()
|
|
68
|
-
if explicit:
|
|
69
|
-
return explicit
|
|
70
|
-
completed = subprocess.run(
|
|
71
|
-
[cli, "config", "show"],
|
|
72
|
-
capture_output=True,
|
|
73
|
-
text=True,
|
|
74
|
-
encoding="utf-8",
|
|
75
|
-
errors="replace",
|
|
76
|
-
env=env,
|
|
77
|
-
timeout=min(timeout, 15.0),
|
|
78
|
-
check=False,
|
|
79
|
-
)
|
|
80
|
-
if completed.returncode != 0:
|
|
81
|
-
return ""
|
|
82
|
-
try:
|
|
83
|
-
root = json.loads(completed.stdout)
|
|
84
|
-
value = root.get("data", {}).get("science_base_url", "")
|
|
85
|
-
except (AttributeError, json.JSONDecodeError):
|
|
86
|
-
return ""
|
|
87
|
-
return str(value).strip()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _catalog_profile(base_url: str, requested: str) -> str:
|
|
91
|
-
if requested != "auto":
|
|
92
|
-
return requested
|
|
93
|
-
try:
|
|
94
|
-
path = urlsplit(base_url).path.rstrip("/").lower()
|
|
95
|
-
except ValueError:
|
|
96
|
-
path = ""
|
|
97
|
-
return "legacy" if path.endswith("/science_tool") else "canonical"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _resolve_catalog_names(tool: str, endpoint: str, profile: str) -> tuple[str, str]:
|
|
101
|
-
if profile == "canonical":
|
|
102
|
-
return tool, endpoint
|
|
103
|
-
canonical_tool = tool.removesuffix("-service") if tool in {"antbo-service", "synbo-service"} else tool
|
|
104
|
-
resolved_tool = LEGACY_TOOL_ALIASES.get(canonical_tool, canonical_tool)
|
|
105
|
-
if endpoint.startswith(("chem_", "biology_")):
|
|
106
|
-
return resolved_tool, endpoint
|
|
107
|
-
resolved_endpoint = LEGACY_ENDPOINTS.get(canonical_tool, {}).get(endpoint)
|
|
108
|
-
if not resolved_endpoint:
|
|
109
|
-
raise ValueError(
|
|
110
|
-
f"no legacy science_tool mapping for {canonical_tool}/{endpoint}"
|
|
111
|
-
)
|
|
112
|
-
return resolved_tool, resolved_endpoint
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _unwrap(stdout: str) -> tuple[Any, int]:
|
|
116
|
-
try:
|
|
117
|
-
root = json.loads(stdout)
|
|
118
|
-
except json.JSONDecodeError as exc:
|
|
119
|
-
raise RuntimeError(f"CLI stdout is not JSON: {exc}") from exc
|
|
120
|
-
if not isinstance(root, dict):
|
|
121
|
-
raise RuntimeError("CLI stdout root is not an object")
|
|
122
|
-
if root.get("ok") is not True:
|
|
123
|
-
raise RuntimeError(json.dumps(root.get("error", root), ensure_ascii=False))
|
|
124
|
-
if "data" not in root:
|
|
125
|
-
raise RuntimeError("CLI success envelope is missing data")
|
|
126
|
-
|
|
127
|
-
value: Any = root["data"]
|
|
128
|
-
depth = 0
|
|
129
|
-
while isinstance(value, dict):
|
|
130
|
-
if "status_code" in value and "data" in value:
|
|
131
|
-
status_code = value.get("status_code")
|
|
132
|
-
if not isinstance(status_code, int) or not 200 <= status_code < 300:
|
|
133
|
-
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
134
|
-
value = value["data"]
|
|
135
|
-
elif (
|
|
136
|
-
"code" in value
|
|
137
|
-
and "data" in value
|
|
138
|
-
and set(value).intersection({"message", "msg"})
|
|
139
|
-
):
|
|
140
|
-
code = value.get("code")
|
|
141
|
-
if code not in (0, "0", None):
|
|
142
|
-
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
143
|
-
value = value["data"]
|
|
144
|
-
else:
|
|
145
|
-
break
|
|
146
|
-
depth += 1
|
|
147
|
-
if depth > 4:
|
|
148
|
-
raise RuntimeError("too many nested service envelopes")
|
|
149
|
-
if isinstance(value, dict) and value.get("valid") is False:
|
|
150
|
-
raise RuntimeError(json.dumps({
|
|
151
|
-
"valid": False,
|
|
152
|
-
"error": value.get("error"),
|
|
153
|
-
"warnings": value.get("warnings"),
|
|
154
|
-
}, ensure_ascii=False))
|
|
155
|
-
return value, depth
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _parser() -> argparse.ArgumentParser:
|
|
159
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
160
|
-
parser.add_argument("--tool", required=True)
|
|
161
|
-
parser.add_argument("--endpoint", required=True)
|
|
162
|
-
body = parser.add_mutually_exclusive_group()
|
|
163
|
-
body.add_argument("--data-json")
|
|
164
|
-
body.add_argument("--params-json")
|
|
165
|
-
parser.add_argument("--cli")
|
|
166
|
-
parser.add_argument("--science-base-url")
|
|
167
|
-
parser.add_argument(
|
|
168
|
-
"--catalog-profile",
|
|
169
|
-
choices=("auto", "canonical", "legacy"),
|
|
170
|
-
default="auto",
|
|
171
|
-
)
|
|
172
|
-
parser.add_argument("--timeout", type=float, default=120.0)
|
|
173
|
-
return parser
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def main() -> int:
|
|
177
|
-
args = _parser().parse_args()
|
|
178
|
-
started = time.perf_counter()
|
|
179
|
-
stage = "arguments"
|
|
180
|
-
profile = "unknown"
|
|
181
|
-
resolved_tool = args.tool
|
|
182
|
-
resolved_endpoint = args.endpoint
|
|
183
|
-
try:
|
|
184
|
-
data = _json_object(args.data_json, "--data-json")
|
|
185
|
-
params = _json_object(args.params_json, "--params-json")
|
|
186
|
-
runtime = _runtime_config()
|
|
187
|
-
stage = "cli-resolution"
|
|
188
|
-
cli = _resolve_cli(args.cli, runtime)
|
|
189
|
-
|
|
190
|
-
env = os.environ.copy()
|
|
191
|
-
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
192
|
-
if base_url:
|
|
193
|
-
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
194
|
-
configured_base_url = _cli_science_base_url(cli, env, args.timeout)
|
|
195
|
-
requested_profile = str(runtime.get("catalog_profile", args.catalog_profile))
|
|
196
|
-
if requested_profile not in {"auto", "canonical", "legacy"}:
|
|
197
|
-
raise ValueError("catalog_profile must be auto, canonical, or legacy")
|
|
198
|
-
profile = _catalog_profile(configured_base_url, requested_profile)
|
|
199
|
-
resolved_tool, resolved_endpoint = _resolve_catalog_names(
|
|
200
|
-
args.tool, args.endpoint, profile
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
command = [
|
|
204
|
-
cli, "science", "invoke",
|
|
205
|
-
"--tool", resolved_tool,
|
|
206
|
-
"--endpoint", resolved_endpoint,
|
|
207
|
-
]
|
|
208
|
-
if data is not None:
|
|
209
|
-
command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
|
|
210
|
-
if params is not None:
|
|
211
|
-
command.extend(["--params", json.dumps(params, ensure_ascii=False, separators=(",", ":"))])
|
|
212
|
-
|
|
213
|
-
stage = "delta-cli"
|
|
214
|
-
completed = subprocess.run(
|
|
215
|
-
command,
|
|
216
|
-
capture_output=True,
|
|
217
|
-
text=True,
|
|
218
|
-
encoding="utf-8",
|
|
219
|
-
errors="replace",
|
|
220
|
-
env=env,
|
|
221
|
-
timeout=args.timeout,
|
|
222
|
-
check=False,
|
|
223
|
-
)
|
|
224
|
-
if completed.returncode != 0:
|
|
225
|
-
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
226
|
-
raise RuntimeError(f"exit_code={completed.returncode}: {detail}")
|
|
227
|
-
stage = "response-validation"
|
|
228
|
-
native, depth = _unwrap(completed.stdout)
|
|
229
|
-
result = {
|
|
230
|
-
"ok": True,
|
|
231
|
-
"transport": "delta-cli",
|
|
232
|
-
"tool": args.tool,
|
|
233
|
-
"endpoint": args.endpoint,
|
|
234
|
-
"resolved_tool": resolved_tool,
|
|
235
|
-
"resolved_endpoint": resolved_endpoint,
|
|
236
|
-
"catalog_profile": profile,
|
|
237
|
-
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
238
|
-
"envelope_depth": depth,
|
|
239
|
-
"native": native,
|
|
240
|
-
}
|
|
241
|
-
print(json.dumps(result, ensure_ascii=False))
|
|
242
|
-
return 0
|
|
243
|
-
except subprocess.TimeoutExpired:
|
|
244
|
-
error = f"timeout after {args.timeout}s"
|
|
245
|
-
except (OSError, ValueError, RuntimeError) as exc:
|
|
246
|
-
error = str(exc)
|
|
247
|
-
print(json.dumps({
|
|
248
|
-
"ok": False,
|
|
249
|
-
"transport": "delta-cli",
|
|
250
|
-
"tool": args.tool,
|
|
251
|
-
"endpoint": args.endpoint,
|
|
252
|
-
"resolved_tool": resolved_tool,
|
|
253
|
-
"resolved_endpoint": resolved_endpoint,
|
|
254
|
-
"catalog_profile": profile,
|
|
255
|
-
"stage": stage,
|
|
256
|
-
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
257
|
-
"error": error[:2000],
|
|
258
|
-
}, ensure_ascii=False))
|
|
259
|
-
return 1
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if __name__ == "__main__":
|
|
263
|
-
sys.exit(main())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
|
|
24
|
+
if raw is None:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
value = json.loads(raw)
|
|
28
|
+
except json.JSONDecodeError as exc:
|
|
29
|
+
raise ValueError(f"{flag} is not valid JSON: {exc}") from exc
|
|
30
|
+
if not isinstance(value, dict):
|
|
31
|
+
raise ValueError(f"{flag} must decode to a JSON object")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _runtime_config() -> dict[str, Any]:
|
|
36
|
+
path = SKILL_DIR / "runtime.local.json"
|
|
37
|
+
if not path.is_file():
|
|
38
|
+
return {}
|
|
39
|
+
try:
|
|
40
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
41
|
+
except (OSError, json.JSONDecodeError):
|
|
42
|
+
return {}
|
|
43
|
+
return value if isinstance(value, dict) else {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
|
|
47
|
+
candidates: list[str | None] = [
|
|
48
|
+
explicit,
|
|
49
|
+
os.environ.get("DELTA_CLI_PATH"),
|
|
50
|
+
runtime.get("cli_path"),
|
|
51
|
+
shutil.which("delta-cli"),
|
|
52
|
+
shutil.which("delta-cli.exe"),
|
|
53
|
+
]
|
|
54
|
+
package_root = SKILL_DIR.parents[1]
|
|
55
|
+
exe_name = "delta-cli.exe" if os.name == "nt" else "delta-cli"
|
|
56
|
+
candidates.append(str(package_root / "bin" / exe_name))
|
|
57
|
+
for candidate in candidates:
|
|
58
|
+
if candidate and Path(candidate).is_file():
|
|
59
|
+
return str(Path(candidate).resolve())
|
|
60
|
+
raise FileNotFoundError(
|
|
61
|
+
"delta-cli not found; install @delta-infra/cli, set DELTA_CLI_PATH, "
|
|
62
|
+
"or add cli_path to delta-science/runtime.local.json"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _cli_science_base_url(cli: str, env: dict[str, str], timeout: float) -> str:
|
|
67
|
+
explicit = env.get("DELTA_INFRA_SCIENCE_BASE_URL", "").strip()
|
|
68
|
+
if explicit:
|
|
69
|
+
return explicit
|
|
70
|
+
completed = subprocess.run(
|
|
71
|
+
[cli, "config", "show"],
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
errors="replace",
|
|
76
|
+
env=env,
|
|
77
|
+
timeout=min(timeout, 15.0),
|
|
78
|
+
check=False,
|
|
79
|
+
)
|
|
80
|
+
if completed.returncode != 0:
|
|
81
|
+
return ""
|
|
82
|
+
try:
|
|
83
|
+
root = json.loads(completed.stdout)
|
|
84
|
+
value = root.get("data", {}).get("science_base_url", "")
|
|
85
|
+
except (AttributeError, json.JSONDecodeError):
|
|
86
|
+
return ""
|
|
87
|
+
return str(value).strip()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _catalog_profile(base_url: str, requested: str) -> str:
|
|
91
|
+
if requested != "auto":
|
|
92
|
+
return requested
|
|
93
|
+
try:
|
|
94
|
+
path = urlsplit(base_url).path.rstrip("/").lower()
|
|
95
|
+
except ValueError:
|
|
96
|
+
path = ""
|
|
97
|
+
return "legacy" if path.endswith("/science_tool") else "canonical"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_catalog_names(tool: str, endpoint: str, profile: str) -> tuple[str, str]:
|
|
101
|
+
if profile == "canonical":
|
|
102
|
+
return tool, endpoint
|
|
103
|
+
canonical_tool = tool.removesuffix("-service") if tool in {"antbo-service", "synbo-service"} else tool
|
|
104
|
+
resolved_tool = LEGACY_TOOL_ALIASES.get(canonical_tool, canonical_tool)
|
|
105
|
+
if endpoint.startswith(("chem_", "biology_")):
|
|
106
|
+
return resolved_tool, endpoint
|
|
107
|
+
resolved_endpoint = LEGACY_ENDPOINTS.get(canonical_tool, {}).get(endpoint)
|
|
108
|
+
if not resolved_endpoint:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"no legacy science_tool mapping for {canonical_tool}/{endpoint}"
|
|
111
|
+
)
|
|
112
|
+
return resolved_tool, resolved_endpoint
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _unwrap(stdout: str) -> tuple[Any, int]:
|
|
116
|
+
try:
|
|
117
|
+
root = json.loads(stdout)
|
|
118
|
+
except json.JSONDecodeError as exc:
|
|
119
|
+
raise RuntimeError(f"CLI stdout is not JSON: {exc}") from exc
|
|
120
|
+
if not isinstance(root, dict):
|
|
121
|
+
raise RuntimeError("CLI stdout root is not an object")
|
|
122
|
+
if root.get("ok") is not True:
|
|
123
|
+
raise RuntimeError(json.dumps(root.get("error", root), ensure_ascii=False))
|
|
124
|
+
if "data" not in root:
|
|
125
|
+
raise RuntimeError("CLI success envelope is missing data")
|
|
126
|
+
|
|
127
|
+
value: Any = root["data"]
|
|
128
|
+
depth = 0
|
|
129
|
+
while isinstance(value, dict):
|
|
130
|
+
if "status_code" in value and "data" in value:
|
|
131
|
+
status_code = value.get("status_code")
|
|
132
|
+
if not isinstance(status_code, int) or not 200 <= status_code < 300:
|
|
133
|
+
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
134
|
+
value = value["data"]
|
|
135
|
+
elif (
|
|
136
|
+
"code" in value
|
|
137
|
+
and "data" in value
|
|
138
|
+
and set(value).intersection({"message", "msg"})
|
|
139
|
+
):
|
|
140
|
+
code = value.get("code")
|
|
141
|
+
if code not in (0, "0", None):
|
|
142
|
+
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
143
|
+
value = value["data"]
|
|
144
|
+
else:
|
|
145
|
+
break
|
|
146
|
+
depth += 1
|
|
147
|
+
if depth > 4:
|
|
148
|
+
raise RuntimeError("too many nested service envelopes")
|
|
149
|
+
if isinstance(value, dict) and value.get("valid") is False:
|
|
150
|
+
raise RuntimeError(json.dumps({
|
|
151
|
+
"valid": False,
|
|
152
|
+
"error": value.get("error"),
|
|
153
|
+
"warnings": value.get("warnings"),
|
|
154
|
+
}, ensure_ascii=False))
|
|
155
|
+
return value, depth
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parser() -> argparse.ArgumentParser:
|
|
159
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
160
|
+
parser.add_argument("--tool", required=True)
|
|
161
|
+
parser.add_argument("--endpoint", required=True)
|
|
162
|
+
body = parser.add_mutually_exclusive_group()
|
|
163
|
+
body.add_argument("--data-json")
|
|
164
|
+
body.add_argument("--params-json")
|
|
165
|
+
parser.add_argument("--cli")
|
|
166
|
+
parser.add_argument("--science-base-url")
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--catalog-profile",
|
|
169
|
+
choices=("auto", "canonical", "legacy"),
|
|
170
|
+
default="auto",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument("--timeout", type=float, default=120.0)
|
|
173
|
+
return parser
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> int:
|
|
177
|
+
args = _parser().parse_args()
|
|
178
|
+
started = time.perf_counter()
|
|
179
|
+
stage = "arguments"
|
|
180
|
+
profile = "unknown"
|
|
181
|
+
resolved_tool = args.tool
|
|
182
|
+
resolved_endpoint = args.endpoint
|
|
183
|
+
try:
|
|
184
|
+
data = _json_object(args.data_json, "--data-json")
|
|
185
|
+
params = _json_object(args.params_json, "--params-json")
|
|
186
|
+
runtime = _runtime_config()
|
|
187
|
+
stage = "cli-resolution"
|
|
188
|
+
cli = _resolve_cli(args.cli, runtime)
|
|
189
|
+
|
|
190
|
+
env = os.environ.copy()
|
|
191
|
+
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
192
|
+
if base_url:
|
|
193
|
+
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
194
|
+
configured_base_url = _cli_science_base_url(cli, env, args.timeout)
|
|
195
|
+
requested_profile = str(runtime.get("catalog_profile", args.catalog_profile))
|
|
196
|
+
if requested_profile not in {"auto", "canonical", "legacy"}:
|
|
197
|
+
raise ValueError("catalog_profile must be auto, canonical, or legacy")
|
|
198
|
+
profile = _catalog_profile(configured_base_url, requested_profile)
|
|
199
|
+
resolved_tool, resolved_endpoint = _resolve_catalog_names(
|
|
200
|
+
args.tool, args.endpoint, profile
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
command = [
|
|
204
|
+
cli, "science", "invoke",
|
|
205
|
+
"--tool", resolved_tool,
|
|
206
|
+
"--endpoint", resolved_endpoint,
|
|
207
|
+
]
|
|
208
|
+
if data is not None:
|
|
209
|
+
command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
|
|
210
|
+
if params is not None:
|
|
211
|
+
command.extend(["--params", json.dumps(params, ensure_ascii=False, separators=(",", ":"))])
|
|
212
|
+
|
|
213
|
+
stage = "delta-cli"
|
|
214
|
+
completed = subprocess.run(
|
|
215
|
+
command,
|
|
216
|
+
capture_output=True,
|
|
217
|
+
text=True,
|
|
218
|
+
encoding="utf-8",
|
|
219
|
+
errors="replace",
|
|
220
|
+
env=env,
|
|
221
|
+
timeout=args.timeout,
|
|
222
|
+
check=False,
|
|
223
|
+
)
|
|
224
|
+
if completed.returncode != 0:
|
|
225
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
226
|
+
raise RuntimeError(f"exit_code={completed.returncode}: {detail}")
|
|
227
|
+
stage = "response-validation"
|
|
228
|
+
native, depth = _unwrap(completed.stdout)
|
|
229
|
+
result = {
|
|
230
|
+
"ok": True,
|
|
231
|
+
"transport": "delta-cli",
|
|
232
|
+
"tool": args.tool,
|
|
233
|
+
"endpoint": args.endpoint,
|
|
234
|
+
"resolved_tool": resolved_tool,
|
|
235
|
+
"resolved_endpoint": resolved_endpoint,
|
|
236
|
+
"catalog_profile": profile,
|
|
237
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
238
|
+
"envelope_depth": depth,
|
|
239
|
+
"native": native,
|
|
240
|
+
}
|
|
241
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
242
|
+
return 0
|
|
243
|
+
except subprocess.TimeoutExpired:
|
|
244
|
+
error = f"timeout after {args.timeout}s"
|
|
245
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
246
|
+
error = str(exc)
|
|
247
|
+
print(json.dumps({
|
|
248
|
+
"ok": False,
|
|
249
|
+
"transport": "delta-cli",
|
|
250
|
+
"tool": args.tool,
|
|
251
|
+
"endpoint": args.endpoint,
|
|
252
|
+
"resolved_tool": resolved_tool,
|
|
253
|
+
"resolved_endpoint": resolved_endpoint,
|
|
254
|
+
"catalog_profile": profile,
|
|
255
|
+
"stage": stage,
|
|
256
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
257
|
+
"error": error[:2000],
|
|
258
|
+
}, ensure_ascii=False))
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
sys.exit(main())
|