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.
Files changed (38) hide show
  1. package/README.md +20 -1
  2. package/README_CN.md +20 -1
  3. package/docs/installation.md +10 -0
  4. package/docs/installation.zh-CN.md +13 -1
  5. package/docs/testing.md +11 -0
  6. package/docs/testing.zh-CN.md +10 -0
  7. package/index.mjs +184 -0
  8. package/package.json +4 -2
  9. package/skills/scnet-hpc/SKILL.md +63 -105
  10. package/skills/scnet-hpc/VERSION +1 -0
  11. package/skills/scnet-hpc/clusters/_template.conf +10 -4
  12. package/skills/scnet-hpc/references/adding-cluster.md +6 -1
  13. package/skills/scnet-hpc/references/backends.md +177 -0
  14. package/skills/scnet-hpc/references/notebook.md +60 -0
  15. package/skills/scnet-hpc/references/openapi.md +140 -0
  16. package/skills/scnet-hpc/references/quickstart-en.md +22 -0
  17. package/skills/scnet-hpc/references/setup.md +4 -0
  18. package/skills/scnet-hpc/scripts/__pycache__/compute-probe.cpython-312.pyc +0 -0
  19. package/skills/scnet-hpc/scripts/__pycache__/scnet.cpython-312.pyc +0 -0
  20. package/skills/scnet-hpc/scripts/__pycache__/scnet_config.cpython-312.pyc +0 -0
  21. package/skills/scnet-hpc/scripts/__pycache__/scnet_credentials.cpython-312.pyc +0 -0
  22. package/skills/scnet-hpc/scripts/__pycache__/scnet_version.cpython-312.pyc +0 -0
  23. package/skills/scnet-hpc/scripts/scnet.py +1573 -0
  24. package/skills/scnet-hpc/scripts/scnet_backends/__init__.py +53 -0
  25. package/skills/scnet-hpc/scripts/scnet_backends/base.py +64 -0
  26. package/skills/scnet-hpc/scripts/scnet_backends/external.py +92 -0
  27. package/skills/scnet-hpc/scripts/scnet_backends/openapi.py +771 -0
  28. package/skills/scnet-hpc/scripts/scnet_backends/profile.py +78 -0
  29. package/skills/scnet-hpc/scripts/scnet_backends/ssh.py +258 -0
  30. package/skills/scnet-hpc/scripts/scnet_config.py +175 -0
  31. package/skills/scnet-hpc/scripts/scnet_credentials.py +160 -0
  32. package/skills/scnet-hpc/scripts/scnet_sdk/__init__.py +11 -0
  33. package/skills/scnet-hpc/scripts/scnet_sdk/cli.py +394 -0
  34. package/skills/scnet-hpc/scripts/scnet_sdk/client.py +240 -0
  35. package/skills/scnet-hpc/scripts/scnet_sdk/notebook.py +379 -0
  36. package/skills/scnet-hpc/scripts/scnet_version.py +20 -0
  37. package/skills/scnet-hpc/scripts/setup-ssh.sh +33 -1
  38. 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)