dsh-scnet 0.4.2 → 0.5.0
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 +20 -1
- package/README_CN.md +20 -1
- package/docs/installation.md +10 -0
- package/docs/installation.zh-CN.md +13 -1
- package/docs/testing.md +11 -0
- package/docs/testing.zh-CN.md +10 -0
- package/index.mjs +184 -0
- package/package.json +4 -2
- package/skills/scnet-hpc/SKILL.md +63 -105
- package/skills/scnet-hpc/VERSION +1 -0
- package/skills/scnet-hpc/clusters/_template.conf +10 -4
- package/skills/scnet-hpc/references/adding-cluster.md +6 -1
- package/skills/scnet-hpc/references/backends.md +177 -0
- package/skills/scnet-hpc/references/notebook.md +60 -0
- package/skills/scnet-hpc/references/openapi.md +140 -0
- package/skills/scnet-hpc/references/quickstart-en.md +22 -0
- package/skills/scnet-hpc/references/setup.md +4 -0
- package/skills/scnet-hpc/scripts/__pycache__/compute-probe.cpython-312.pyc +0 -0
- package/skills/scnet-hpc/scripts/__pycache__/scnet.cpython-312.pyc +0 -0
- package/skills/scnet-hpc/scripts/__pycache__/scnet_config.cpython-312.pyc +0 -0
- package/skills/scnet-hpc/scripts/__pycache__/scnet_credentials.cpython-312.pyc +0 -0
- package/skills/scnet-hpc/scripts/__pycache__/scnet_version.cpython-312.pyc +0 -0
- package/skills/scnet-hpc/scripts/scnet.py +1573 -0
- package/skills/scnet-hpc/scripts/scnet_backends/__init__.py +53 -0
- package/skills/scnet-hpc/scripts/scnet_backends/base.py +64 -0
- package/skills/scnet-hpc/scripts/scnet_backends/external.py +92 -0
- package/skills/scnet-hpc/scripts/scnet_backends/openapi.py +771 -0
- package/skills/scnet-hpc/scripts/scnet_backends/profile.py +78 -0
- package/skills/scnet-hpc/scripts/scnet_backends/ssh.py +258 -0
- package/skills/scnet-hpc/scripts/scnet_config.py +175 -0
- package/skills/scnet-hpc/scripts/scnet_credentials.py +160 -0
- package/skills/scnet-hpc/scripts/scnet_sdk/__init__.py +11 -0
- package/skills/scnet-hpc/scripts/scnet_sdk/cli.py +394 -0
- package/skills/scnet-hpc/scripts/scnet_sdk/client.py +240 -0
- package/skills/scnet-hpc/scripts/scnet_sdk/notebook.py +379 -0
- package/skills/scnet-hpc/scripts/scnet_version.py +20 -0
- package/skills/scnet-hpc/scripts/setup-ssh.sh +33 -1
- package/skills/scnet-hpc/scripts/setup.sh +471 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Backend registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import BackendContext, BackendError
|
|
6
|
+
from .external import ExternalBackend, discover_external_backends
|
|
7
|
+
from .openapi import OpenAPIBackend
|
|
8
|
+
from .ssh import SSHBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
BUILTINS = {
|
|
12
|
+
"ssh": SSHBackend,
|
|
13
|
+
"openapi": OpenAPIBackend,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def backend_catalog() -> list[dict[str, object]]:
|
|
18
|
+
result = [
|
|
19
|
+
{
|
|
20
|
+
"name": name,
|
|
21
|
+
"kind": "builtin",
|
|
22
|
+
"capabilities": sorted(backend.capabilities),
|
|
23
|
+
}
|
|
24
|
+
for name, backend in BUILTINS.items()
|
|
25
|
+
]
|
|
26
|
+
for name, executable in sorted(discover_external_backends().items()):
|
|
27
|
+
result.append(
|
|
28
|
+
{
|
|
29
|
+
"name": name,
|
|
30
|
+
"kind": "external",
|
|
31
|
+
"executable": executable,
|
|
32
|
+
"capabilities": "queried when selected",
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_backend(name: str, context: BackendContext):
|
|
39
|
+
if name in BUILTINS:
|
|
40
|
+
return BUILTINS[name](context)
|
|
41
|
+
external = discover_external_backends()
|
|
42
|
+
if name in external:
|
|
43
|
+
return ExternalBackend(context, name, external[name])
|
|
44
|
+
available = ", ".join(item["name"] for item in backend_catalog())
|
|
45
|
+
raise BackendError(f"unknown backend {name!r}; available: {available}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"BackendContext",
|
|
50
|
+
"BackendError",
|
|
51
|
+
"backend_catalog",
|
|
52
|
+
"create_backend",
|
|
53
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared backend interfaces for the SCNet command-line client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PROTOCOL_VERSION = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BackendError(RuntimeError):
|
|
14
|
+
"""A bounded, user-facing backend failure."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BackendContext:
|
|
19
|
+
repo_root: Path
|
|
20
|
+
profile_name: str | None
|
|
21
|
+
profile: Mapping[str, str]
|
|
22
|
+
timeout: int = 30
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Backend:
|
|
26
|
+
name = "base"
|
|
27
|
+
capabilities: frozenset[str] = frozenset()
|
|
28
|
+
|
|
29
|
+
def __init__(self, context: BackendContext):
|
|
30
|
+
self.context = context
|
|
31
|
+
|
|
32
|
+
def execute(self, operation: str, options: Mapping[str, Any]) -> Any:
|
|
33
|
+
if operation not in self.capabilities:
|
|
34
|
+
raise BackendError(
|
|
35
|
+
f"backend {self.name!r} does not support operation {operation!r}"
|
|
36
|
+
)
|
|
37
|
+
handler = getattr(self, f"op_{operation.replace('-', '_')}", None)
|
|
38
|
+
if handler is None:
|
|
39
|
+
raise BackendError(
|
|
40
|
+
f"backend {self.name!r} advertises {operation!r} but has no handler"
|
|
41
|
+
)
|
|
42
|
+
return handler(options)
|
|
43
|
+
|
|
44
|
+
def preview(self, operation: str, options: Mapping[str, Any]) -> Any:
|
|
45
|
+
"""Describe a mutation without contacting the backend."""
|
|
46
|
+
if operation not in self.capabilities:
|
|
47
|
+
raise BackendError(
|
|
48
|
+
f"backend {self.name!r} does not support operation {operation!r}"
|
|
49
|
+
)
|
|
50
|
+
handler = getattr(self, f"preview_{operation.replace('-', '_')}", None)
|
|
51
|
+
if handler is not None:
|
|
52
|
+
return handler(options)
|
|
53
|
+
return {
|
|
54
|
+
"backend": self.name,
|
|
55
|
+
"operation": operation,
|
|
56
|
+
"options": dict(options),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def require_option(options: Mapping[str, Any], name: str) -> Any:
|
|
61
|
+
value = options.get(name)
|
|
62
|
+
if value is None or value == "":
|
|
63
|
+
raise BackendError(f"missing required option: {name}")
|
|
64
|
+
return value
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Executable adapter for MCP bridges and other future connectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .base import Backend, BackendError, PROTOCOL_VERSION
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PREFIX = "scnet-hpc-backend-"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def discover_external_backends() -> dict[str, str]:
|
|
19
|
+
found: dict[str, str] = {}
|
|
20
|
+
search_dirs: list[Path] = []
|
|
21
|
+
for item in os.environ.get("SCNET_HPC_BACKEND_PATH", "").split(os.pathsep):
|
|
22
|
+
if item:
|
|
23
|
+
search_dirs.append(Path(item).expanduser())
|
|
24
|
+
for directory in search_dirs:
|
|
25
|
+
if not directory.is_dir():
|
|
26
|
+
continue
|
|
27
|
+
for path in directory.glob(f"{PREFIX}*"):
|
|
28
|
+
if path.is_file() and os.access(path, os.X_OK):
|
|
29
|
+
found[path.name[len(PREFIX) :]] = str(path)
|
|
30
|
+
for name in filter(
|
|
31
|
+
None,
|
|
32
|
+
(item.strip() for item in os.environ.get(
|
|
33
|
+
"SCNET_HPC_EXTERNAL_BACKENDS", ""
|
|
34
|
+
).split(",")),
|
|
35
|
+
):
|
|
36
|
+
executable = shutil.which(f"{PREFIX}{name}")
|
|
37
|
+
if executable:
|
|
38
|
+
found[name] = executable
|
|
39
|
+
return found
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExternalBackend(Backend):
|
|
43
|
+
def __init__(self, context, name: str, executable: str):
|
|
44
|
+
super().__init__(context)
|
|
45
|
+
self.name = name
|
|
46
|
+
self.executable = executable
|
|
47
|
+
self.capabilities = frozenset(self._request("capabilities", {}).get("capabilities", []))
|
|
48
|
+
|
|
49
|
+
def _request(self, operation: str, options: Mapping[str, Any]) -> Any:
|
|
50
|
+
request = {
|
|
51
|
+
"protocol": "scnet-hpc.backend",
|
|
52
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
53
|
+
"operation": operation,
|
|
54
|
+
"profile_name": self.context.profile_name,
|
|
55
|
+
"profile": dict(self.context.profile),
|
|
56
|
+
"options": dict(options),
|
|
57
|
+
}
|
|
58
|
+
try:
|
|
59
|
+
completed = subprocess.run(
|
|
60
|
+
[self.executable],
|
|
61
|
+
input=json.dumps(request, ensure_ascii=False),
|
|
62
|
+
text=True,
|
|
63
|
+
stdout=subprocess.PIPE,
|
|
64
|
+
stderr=subprocess.PIPE,
|
|
65
|
+
timeout=self.context.timeout,
|
|
66
|
+
check=False,
|
|
67
|
+
)
|
|
68
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
69
|
+
raise BackendError(str(exc)) from exc
|
|
70
|
+
if completed.returncode != 0:
|
|
71
|
+
raise BackendError(
|
|
72
|
+
completed.stderr.strip()
|
|
73
|
+
or f"external backend exited with {completed.returncode}"
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
response = json.loads(completed.stdout)
|
|
77
|
+
except json.JSONDecodeError as exc:
|
|
78
|
+
raise BackendError("external backend returned invalid JSON") from exc
|
|
79
|
+
if not isinstance(response, dict):
|
|
80
|
+
raise BackendError("external backend response must be a JSON object")
|
|
81
|
+
if response.get("protocol_version") not in (None, PROTOCOL_VERSION):
|
|
82
|
+
raise BackendError("external backend protocol version mismatch")
|
|
83
|
+
if response.get("ok") is False:
|
|
84
|
+
raise BackendError(str(response.get("error") or "external backend failed"))
|
|
85
|
+
return response.get("data", response)
|
|
86
|
+
|
|
87
|
+
def execute(self, operation: str, options: Mapping[str, Any]) -> Any:
|
|
88
|
+
if operation not in self.capabilities:
|
|
89
|
+
raise BackendError(
|
|
90
|
+
f"backend {self.name!r} does not support operation {operation!r}"
|
|
91
|
+
)
|
|
92
|
+
return self._request(operation, options)
|