splime 0.1.2__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.
- spl/__init__.py +14 -0
- spl/client.py +1364 -0
- spl/core/__init__.py +23 -0
- spl/core/common.py +350 -0
- spl/core/entities/__init__.py +0 -0
- spl/core/entities/adapter.py +210 -0
- spl/core/entities/artifact.py +141 -0
- spl/core/entities/control.py +45 -0
- spl/core/entities/distribution.py +65 -0
- spl/core/entities/function.py +254 -0
- spl/core/entities/local_function.py +286 -0
- spl/core/entities/misc.py +14 -0
- spl/core/entities/module.py +88 -0
- spl/core/entities/node.py +286 -0
- spl/core/entities/node_function.py +79 -0
- spl/core/entities/node_remote.py +295 -0
- spl/core/entities/pipeline.py +436 -0
- spl/core/entities/scalar.py +55 -0
- spl/core/ir/__init__.py +0 -0
- spl/core/ir/common.py +34 -0
- spl/core/ir/parse.py +79 -0
- spl/core/ir/unparse.py +29 -0
- spl/core/ir/utils.py +163 -0
- spl/daemon/__init__.py +23 -0
- spl/daemon/__main__.py +11 -0
- spl/daemon/cli.py +582 -0
- spl/daemon/client.py +43 -0
- spl/daemon/docker_environment.py +329 -0
- spl/daemon/docker_pool.py +516 -0
- spl/daemon/environment.py +228 -0
- spl/daemon/environment_base.py +479 -0
- spl/daemon/heartbeat_service.py +119 -0
- spl/daemon/metadata.py +427 -0
- spl/daemon/remote_client.py +457 -0
- spl/daemon/repositories/__init__.py +17 -0
- spl/daemon/repositories/env.py +323 -0
- spl/daemon/repositories/library.py +181 -0
- spl/daemon/repositories/object.py +997 -0
- spl/daemon/repositories/run.py +279 -0
- spl/daemon/repositories/server_connection.py +657 -0
- spl/daemon/repositories/sync_event.py +129 -0
- spl/daemon/routes/__init__.py +1 -0
- spl/daemon/routes/_helpers.py +147 -0
- spl/daemon/routes/artifacts.py +77 -0
- spl/daemon/routes/diagnostics.py +114 -0
- spl/daemon/routes/envs.py +82 -0
- spl/daemon/routes/libraries.py +129 -0
- spl/daemon/routes/objects.py +174 -0
- spl/daemon/routes/remote.py +56 -0
- spl/daemon/routes/runs.py +96 -0
- spl/daemon/routes/server_connections.py +86 -0
- spl/daemon/runtime_backend.py +368 -0
- spl/daemon/runtime_config.py +133 -0
- spl/daemon/runtime_dependencies.py +459 -0
- spl/daemon/secret_store.py +187 -0
- spl/daemon/server.py +2224 -0
- spl/daemon/server_connection.py +267 -0
- spl/daemon/services/__init__.py +1 -0
- spl/daemon/services/sync.py +76 -0
- spl/daemon/signature.py +376 -0
- spl/daemon/storage_base.py +542 -0
- spl/daemon/store.py +436 -0
- spl/daemon/worker.py +526 -0
- spl/daemon_client.py +945 -0
- spl/pipeline_widget.py +1452 -0
- spl/py.typed +0 -0
- spl/server_client.py +787 -0
- splime-0.1.2.dist-info/METADATA +189 -0
- splime-0.1.2.dist-info/RECORD +74 -0
- splime-0.1.2.dist-info/WHEEL +5 -0
- splime-0.1.2.dist-info/entry_points.txt +2 -0
- splime-0.1.2.dist-info/licenses/LICENSE +201 -0
- splime-0.1.2.dist-info/licenses/NOTICE +8 -0
- splime-0.1.2.dist-info/top_level.txt +1 -0
spl/daemon/cli.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""Command line interface for the minimal SPL daemon.
|
|
2
|
+
|
|
3
|
+
The CLI is intentionally thin and maps one-to-one to the client/server API. It
|
|
4
|
+
is enough for the MVP workflow:
|
|
5
|
+
|
|
6
|
+
1. start the local daemon;
|
|
7
|
+
2. register a Python environment;
|
|
8
|
+
3. register a serialized SPL YAML object;
|
|
9
|
+
4. run that object and fetch the result/artifacts.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from spl.daemon_client import DEFAULT_DAEMON_PORT, DEFAULT_SERVER_URL, DEFAULT_URL, Client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_json(value: Any) -> None:
|
|
24
|
+
"""Print a JSON value for shell-friendly output."""
|
|
25
|
+
|
|
26
|
+
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_json_arg(value: str, expected_type: type) -> Any:
|
|
30
|
+
"""Parse and validate a JSON command line argument."""
|
|
31
|
+
|
|
32
|
+
parsed = json.loads(value)
|
|
33
|
+
if not isinstance(parsed, expected_type):
|
|
34
|
+
raise argparse.ArgumentTypeError(
|
|
35
|
+
f"expected JSON {expected_type.__name__}, got {type(parsed).__name__}"
|
|
36
|
+
)
|
|
37
|
+
return parsed
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_runtime_config(
|
|
41
|
+
path: Path | None,
|
|
42
|
+
*,
|
|
43
|
+
runtime: str | None,
|
|
44
|
+
python: str | None,
|
|
45
|
+
base_image: str | None,
|
|
46
|
+
) -> dict[str, Any] | None:
|
|
47
|
+
"""Read a runtime sidecar YAML file and merge explicit CLI overrides."""
|
|
48
|
+
|
|
49
|
+
config: dict[str, Any] = {}
|
|
50
|
+
if path is not None:
|
|
51
|
+
import yaml
|
|
52
|
+
|
|
53
|
+
loaded = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
54
|
+
if loaded is None:
|
|
55
|
+
config = {}
|
|
56
|
+
elif isinstance(loaded, dict):
|
|
57
|
+
config = loaded
|
|
58
|
+
else:
|
|
59
|
+
raise argparse.ArgumentTypeError(
|
|
60
|
+
"--runtime-config must contain a YAML mapping"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if "runtime" in config and isinstance(config["runtime"], dict):
|
|
64
|
+
target = dict(config["runtime"])
|
|
65
|
+
config = {"runtime": target}
|
|
66
|
+
else:
|
|
67
|
+
target = config
|
|
68
|
+
if runtime is not None:
|
|
69
|
+
target["mode"] = runtime
|
|
70
|
+
if python is not None:
|
|
71
|
+
target["python"] = python
|
|
72
|
+
if base_image is not None:
|
|
73
|
+
target["base_image"] = base_image
|
|
74
|
+
|
|
75
|
+
if not config and runtime is None and python is None and base_image is None:
|
|
76
|
+
return None
|
|
77
|
+
return config
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
81
|
+
"""Create the top-level CLI parser."""
|
|
82
|
+
|
|
83
|
+
parser = argparse.ArgumentParser(prog="python -m spl.daemon")
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--url",
|
|
86
|
+
default=None,
|
|
87
|
+
help=f"daemon base URL for client commands; defaults to the running daemon endpoint or {DEFAULT_URL}",
|
|
88
|
+
)
|
|
89
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
90
|
+
|
|
91
|
+
serve = subparsers.add_parser("serve", help="start the local daemon")
|
|
92
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
93
|
+
serve.add_argument("--port", type=int, default=DEFAULT_DAEMON_PORT)
|
|
94
|
+
serve.add_argument("--home", type=Path, default=None)
|
|
95
|
+
serve.add_argument(
|
|
96
|
+
"--auto-port",
|
|
97
|
+
dest="auto_port",
|
|
98
|
+
action="store_true",
|
|
99
|
+
default=True,
|
|
100
|
+
help="try the next ports when the requested port is busy",
|
|
101
|
+
)
|
|
102
|
+
serve.add_argument(
|
|
103
|
+
"--no-auto-port",
|
|
104
|
+
dest="auto_port",
|
|
105
|
+
action="store_false",
|
|
106
|
+
help="fail when the requested port is busy",
|
|
107
|
+
)
|
|
108
|
+
serve.add_argument(
|
|
109
|
+
"--port-scan-limit",
|
|
110
|
+
type=int,
|
|
111
|
+
default=100,
|
|
112
|
+
help="maximum number of sequential ports to try when auto-port is enabled",
|
|
113
|
+
)
|
|
114
|
+
serve.add_argument(
|
|
115
|
+
"--auto-build-envs",
|
|
116
|
+
dest="auto_build_envs",
|
|
117
|
+
action="store_true",
|
|
118
|
+
default=True,
|
|
119
|
+
help="build cached venvs immediately after object registration",
|
|
120
|
+
)
|
|
121
|
+
serve.add_argument(
|
|
122
|
+
"--no-auto-build-envs",
|
|
123
|
+
dest="auto_build_envs",
|
|
124
|
+
action="store_false",
|
|
125
|
+
help="build cached venvs only when an object is first run",
|
|
126
|
+
)
|
|
127
|
+
serve.add_argument(
|
|
128
|
+
"--env-build-timeout",
|
|
129
|
+
type=float,
|
|
130
|
+
default=None,
|
|
131
|
+
help="seconds before venv creation or pip install is failed",
|
|
132
|
+
)
|
|
133
|
+
serve.add_argument(
|
|
134
|
+
"--env-stale-lock-timeout",
|
|
135
|
+
type=float,
|
|
136
|
+
default=None,
|
|
137
|
+
help="seconds before an abandoned venv build lock may be reused",
|
|
138
|
+
)
|
|
139
|
+
serve.add_argument(
|
|
140
|
+
"--docker-pool-size",
|
|
141
|
+
type=int,
|
|
142
|
+
default=0,
|
|
143
|
+
help="maximum warm Docker containers to keep; 0 disables the pool",
|
|
144
|
+
)
|
|
145
|
+
serve.add_argument(
|
|
146
|
+
"--docker-idle-timeout",
|
|
147
|
+
type=float,
|
|
148
|
+
default=300.0,
|
|
149
|
+
help="seconds before an idle warm Docker container is evicted",
|
|
150
|
+
)
|
|
151
|
+
serve.add_argument(
|
|
152
|
+
"--docker-prewarm",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="after Docker object registration, build the image and warm a pooled container",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
subparsers.add_parser("health", help="show daemon health details")
|
|
158
|
+
|
|
159
|
+
server_connect = subparsers.add_parser(
|
|
160
|
+
"server-connect",
|
|
161
|
+
help="connect the local daemon to the central server",
|
|
162
|
+
)
|
|
163
|
+
server_connect.add_argument("--server-url", default=None)
|
|
164
|
+
server_connect.add_argument("--machine-token", required=True)
|
|
165
|
+
server_connect.add_argument("--user-token", required=True)
|
|
166
|
+
server_connect.add_argument("--machine-id", default=None)
|
|
167
|
+
server_connect.add_argument("--display-name", default=None)
|
|
168
|
+
server_connect.add_argument("--capabilities", default="{}", help="JSON object")
|
|
169
|
+
server_connect.add_argument("--heartbeat-interval", type=float, default=None)
|
|
170
|
+
|
|
171
|
+
subparsers.add_parser(
|
|
172
|
+
"server-disconnect",
|
|
173
|
+
help="disconnect the local daemon from the central server",
|
|
174
|
+
)
|
|
175
|
+
subparsers.add_parser(
|
|
176
|
+
"server-connection",
|
|
177
|
+
help="show the current central-server connection",
|
|
178
|
+
)
|
|
179
|
+
subparsers.add_parser(
|
|
180
|
+
"server-connections",
|
|
181
|
+
help="list stored central-server connection attempts",
|
|
182
|
+
)
|
|
183
|
+
subparsers.add_parser(
|
|
184
|
+
"server-machines",
|
|
185
|
+
help="list machines visible to the connected user",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
env_add = subparsers.add_parser("env-add", help="register a Python executable")
|
|
189
|
+
env_add.add_argument("name")
|
|
190
|
+
env_add.add_argument("python")
|
|
191
|
+
|
|
192
|
+
subparsers.add_parser("env-list", help="list registered environments")
|
|
193
|
+
|
|
194
|
+
subparsers.add_parser("env-build-list", help="list cached venv builds")
|
|
195
|
+
|
|
196
|
+
subparsers.add_parser("image-list", help="list cached Docker runtime images")
|
|
197
|
+
|
|
198
|
+
image_show = subparsers.add_parser(
|
|
199
|
+
"image-show",
|
|
200
|
+
help="show one cached Docker runtime image record",
|
|
201
|
+
)
|
|
202
|
+
image_show.add_argument("spec_hash")
|
|
203
|
+
|
|
204
|
+
image_rebuild = subparsers.add_parser(
|
|
205
|
+
"image-rebuild",
|
|
206
|
+
help="force one cached Docker runtime image to be rebuilt",
|
|
207
|
+
)
|
|
208
|
+
image_rebuild.add_argument("spec_hash")
|
|
209
|
+
image_rebuild.add_argument(
|
|
210
|
+
"--wait",
|
|
211
|
+
action="store_true",
|
|
212
|
+
help="wait until the rebuild finishes",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
image_prune = subparsers.add_parser(
|
|
216
|
+
"image-prune",
|
|
217
|
+
help="remove cached Docker runtime images",
|
|
218
|
+
)
|
|
219
|
+
image_prune.add_argument(
|
|
220
|
+
"spec_hash",
|
|
221
|
+
nargs="?",
|
|
222
|
+
default=None,
|
|
223
|
+
help="optional Docker image spec hash to prune; omit to prune all Docker images",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
env_build_show = subparsers.add_parser(
|
|
227
|
+
"env-build-show",
|
|
228
|
+
help="show one cached venv build",
|
|
229
|
+
)
|
|
230
|
+
env_build_show.add_argument("spec_hash")
|
|
231
|
+
|
|
232
|
+
env_build_rebuild = subparsers.add_parser(
|
|
233
|
+
"env-build-rebuild",
|
|
234
|
+
help="force one cached venv build to be recreated",
|
|
235
|
+
)
|
|
236
|
+
env_build_rebuild.add_argument("spec_hash")
|
|
237
|
+
env_build_rebuild.add_argument(
|
|
238
|
+
"--wait",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="wait until the rebuild finishes",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
subparsers.add_parser(
|
|
244
|
+
"remote-signature-list",
|
|
245
|
+
help="list cached remote object signatures",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
remote_signature_resolve = subparsers.add_parser(
|
|
249
|
+
"remote-signature-resolve",
|
|
250
|
+
help="resolve and cache a remote object signature",
|
|
251
|
+
)
|
|
252
|
+
remote_signature_resolve.add_argument(
|
|
253
|
+
"ref",
|
|
254
|
+
help="JSON object, for example {\"url\":\"https://splime.io/api\",\"name\":\"demo\"}",
|
|
255
|
+
)
|
|
256
|
+
remote_signature_resolve.add_argument(
|
|
257
|
+
"--force",
|
|
258
|
+
action="store_true",
|
|
259
|
+
help="refresh the signature even when a cache entry exists",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
object_add = subparsers.add_parser("object-add", help="register SPL YAML")
|
|
263
|
+
object_add.add_argument("name")
|
|
264
|
+
object_add.add_argument("yaml_path", type=Path)
|
|
265
|
+
object_add.add_argument("--entrypoint", required=True)
|
|
266
|
+
object_add.add_argument("--env", required=True)
|
|
267
|
+
object_add.add_argument("--description", default=None)
|
|
268
|
+
object_add.add_argument("--version-label", default=None)
|
|
269
|
+
object_add.add_argument(
|
|
270
|
+
"--runtime",
|
|
271
|
+
choices=["venv", "docker"],
|
|
272
|
+
default=None,
|
|
273
|
+
help="runtime launcher for this object version; defaults to venv",
|
|
274
|
+
)
|
|
275
|
+
object_add.add_argument(
|
|
276
|
+
"--python",
|
|
277
|
+
dest="python_version",
|
|
278
|
+
default=None,
|
|
279
|
+
help="Python version for Docker runtime, for example 3.13",
|
|
280
|
+
)
|
|
281
|
+
object_add.add_argument(
|
|
282
|
+
"--base-image",
|
|
283
|
+
default=None,
|
|
284
|
+
help="Docker base image override, for example python:3.13-slim-trixie",
|
|
285
|
+
)
|
|
286
|
+
object_add.add_argument(
|
|
287
|
+
"--runtime-config",
|
|
288
|
+
type=Path,
|
|
289
|
+
default=None,
|
|
290
|
+
help="YAML sidecar with a top-level runtime mapping",
|
|
291
|
+
)
|
|
292
|
+
object_add.add_argument(
|
|
293
|
+
"--object-id",
|
|
294
|
+
default=None,
|
|
295
|
+
help="explicitly append a version to this existing object id",
|
|
296
|
+
)
|
|
297
|
+
object_add.add_argument(
|
|
298
|
+
"--workdir",
|
|
299
|
+
default=None,
|
|
300
|
+
help="optional worker cwd; defaults to the per-run directory",
|
|
301
|
+
)
|
|
302
|
+
object_add.add_argument(
|
|
303
|
+
"--local-only",
|
|
304
|
+
action="store_true",
|
|
305
|
+
help="store only in the local daemon and skip server sync",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
object_list = subparsers.add_parser("object-list", help="list registered objects")
|
|
309
|
+
object_list.add_argument("--query", default=None, help="optional registry search text")
|
|
310
|
+
object_list.add_argument(
|
|
311
|
+
"--compact",
|
|
312
|
+
action="store_true",
|
|
313
|
+
help="show human-sized object summaries",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
object_show = subparsers.add_parser("object-show", help="show one object")
|
|
317
|
+
object_show.add_argument("name_or_id")
|
|
318
|
+
object_show.add_argument("--version", type=int, default=None)
|
|
319
|
+
|
|
320
|
+
object_signature = subparsers.add_parser(
|
|
321
|
+
"object-signature",
|
|
322
|
+
help="show object inputs, outputs, and result accessors",
|
|
323
|
+
)
|
|
324
|
+
object_signature.add_argument("name_or_id")
|
|
325
|
+
object_signature.add_argument("--version", type=int, default=None)
|
|
326
|
+
|
|
327
|
+
object_inputs = subparsers.add_parser(
|
|
328
|
+
"object-inputs",
|
|
329
|
+
help="show object call inputs",
|
|
330
|
+
)
|
|
331
|
+
object_inputs.add_argument("name_or_id")
|
|
332
|
+
object_inputs.add_argument("--version", type=int, default=None)
|
|
333
|
+
|
|
334
|
+
object_outputs = subparsers.add_parser(
|
|
335
|
+
"object-outputs",
|
|
336
|
+
help="show object output selectors and result accessors",
|
|
337
|
+
)
|
|
338
|
+
object_outputs.add_argument("name_or_id")
|
|
339
|
+
object_outputs.add_argument("--version", type=int, default=None)
|
|
340
|
+
|
|
341
|
+
object_versions = subparsers.add_parser(
|
|
342
|
+
"object-versions",
|
|
343
|
+
help="list versions for one object",
|
|
344
|
+
)
|
|
345
|
+
object_versions.add_argument("name_or_id")
|
|
346
|
+
|
|
347
|
+
run = subparsers.add_parser("run", help="start an object run")
|
|
348
|
+
run.add_argument("object")
|
|
349
|
+
run.add_argument("--args", default="[]", help="JSON list of positional args")
|
|
350
|
+
run.add_argument("--kwargs", default="{}", help="JSON object of keyword args")
|
|
351
|
+
run.add_argument("--output", default=None, help="pipeline alias to return")
|
|
352
|
+
run.add_argument("--timeout", type=float, default=None)
|
|
353
|
+
run.add_argument("--version", type=int, default=None)
|
|
354
|
+
run.add_argument("--version-id", default=None)
|
|
355
|
+
run.add_argument(
|
|
356
|
+
"--target-machine",
|
|
357
|
+
default=None,
|
|
358
|
+
help="run through the central server on this machine id",
|
|
359
|
+
)
|
|
360
|
+
run.add_argument(
|
|
361
|
+
"--source",
|
|
362
|
+
choices=["auto", "local"],
|
|
363
|
+
default="auto",
|
|
364
|
+
help="for local runs, refresh from server before running or stay local only",
|
|
365
|
+
)
|
|
366
|
+
run.add_argument(
|
|
367
|
+
"--wait",
|
|
368
|
+
action="store_true",
|
|
369
|
+
help="wait for completion and print the final result when available",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
run_status = subparsers.add_parser("run-status", help="show one run state")
|
|
373
|
+
run_status.add_argument("run_id")
|
|
374
|
+
|
|
375
|
+
subparsers.add_parser("run-list", help="list known runs")
|
|
376
|
+
|
|
377
|
+
run_result = subparsers.add_parser("run-result", help="show one run result")
|
|
378
|
+
run_result.add_argument("run_id")
|
|
379
|
+
|
|
380
|
+
artifact_list = subparsers.add_parser("artifact-list", help="list run artifacts")
|
|
381
|
+
artifact_list.add_argument("run_id")
|
|
382
|
+
|
|
383
|
+
artifact_get = subparsers.add_parser("artifact-get", help="download an artifact")
|
|
384
|
+
artifact_get.add_argument("run_id")
|
|
385
|
+
artifact_get.add_argument("name")
|
|
386
|
+
artifact_get.add_argument("target", type=Path)
|
|
387
|
+
|
|
388
|
+
return parser
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main(argv: list[str] | None = None) -> int:
|
|
392
|
+
"""Execute the CLI command."""
|
|
393
|
+
|
|
394
|
+
args = build_parser().parse_args(argv)
|
|
395
|
+
|
|
396
|
+
if args.command == "serve":
|
|
397
|
+
try:
|
|
398
|
+
from spl.daemon.server import serve
|
|
399
|
+
|
|
400
|
+
serve(
|
|
401
|
+
host=args.host,
|
|
402
|
+
port=args.port,
|
|
403
|
+
home=args.home,
|
|
404
|
+
auto_port=args.auto_port,
|
|
405
|
+
port_scan_limit=args.port_scan_limit,
|
|
406
|
+
auto_build_envs=args.auto_build_envs,
|
|
407
|
+
env_build_timeout_seconds=args.env_build_timeout,
|
|
408
|
+
env_stale_lock_seconds=args.env_stale_lock_timeout,
|
|
409
|
+
docker_pool_size=args.docker_pool_size,
|
|
410
|
+
docker_idle_timeout_seconds=args.docker_idle_timeout,
|
|
411
|
+
docker_prewarm=args.docker_prewarm,
|
|
412
|
+
)
|
|
413
|
+
return 0
|
|
414
|
+
except Exception as exc: # noqa: BLE001 - CLI should show concise errors.
|
|
415
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
416
|
+
return 1
|
|
417
|
+
|
|
418
|
+
client = Client(args.url)
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
if args.command == "health":
|
|
422
|
+
print_json(client.health())
|
|
423
|
+
elif args.command == "server-connect":
|
|
424
|
+
capabilities = parse_json_arg(args.capabilities, dict)
|
|
425
|
+
print_json(
|
|
426
|
+
client.connect_server(
|
|
427
|
+
machine_token=args.machine_token,
|
|
428
|
+
user_token=args.user_token,
|
|
429
|
+
server_url=args.server_url
|
|
430
|
+
if args.server_url is not None
|
|
431
|
+
else DEFAULT_SERVER_URL,
|
|
432
|
+
machine_id=args.machine_id,
|
|
433
|
+
display_name=args.display_name,
|
|
434
|
+
capabilities=capabilities,
|
|
435
|
+
heartbeat_interval_seconds=args.heartbeat_interval,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
elif args.command == "server-disconnect":
|
|
439
|
+
print_json(client.disconnect_server())
|
|
440
|
+
elif args.command == "server-connection":
|
|
441
|
+
print_json(client.server_connection())
|
|
442
|
+
elif args.command == "server-connections":
|
|
443
|
+
print_json(client.server_connections())
|
|
444
|
+
elif args.command == "server-machines":
|
|
445
|
+
print_json(client.server_machines())
|
|
446
|
+
elif args.command == "env-add":
|
|
447
|
+
print_json(client.register_env(args.name, args.python))
|
|
448
|
+
elif args.command == "env-list":
|
|
449
|
+
print_json(client.list_envs())
|
|
450
|
+
elif args.command == "env-build-list":
|
|
451
|
+
print_json(client.list_environment_builds())
|
|
452
|
+
elif args.command == "image-list":
|
|
453
|
+
print_json(
|
|
454
|
+
[
|
|
455
|
+
record
|
|
456
|
+
for record in client.list_environment_builds()
|
|
457
|
+
if record.get("runtime_type") == "docker"
|
|
458
|
+
]
|
|
459
|
+
)
|
|
460
|
+
elif args.command == "image-show":
|
|
461
|
+
record = client.get_environment_build(args.spec_hash)
|
|
462
|
+
if record.get("runtime_type") != "docker":
|
|
463
|
+
raise ValueError(f"environment build is not a Docker image: {args.spec_hash}")
|
|
464
|
+
print_json(record)
|
|
465
|
+
elif args.command == "image-rebuild":
|
|
466
|
+
print_json(
|
|
467
|
+
client.rebuild_environment_build(
|
|
468
|
+
args.spec_hash,
|
|
469
|
+
wait=args.wait,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
elif args.command == "image-prune":
|
|
473
|
+
print_json(client.prune_docker_images(spec_hash=args.spec_hash))
|
|
474
|
+
elif args.command == "env-build-show":
|
|
475
|
+
print_json(client.get_environment_build(args.spec_hash))
|
|
476
|
+
elif args.command == "env-build-rebuild":
|
|
477
|
+
print_json(
|
|
478
|
+
client.rebuild_environment_build(
|
|
479
|
+
args.spec_hash,
|
|
480
|
+
wait=args.wait,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
elif args.command == "remote-signature-list":
|
|
484
|
+
print_json(client.list_remote_signatures())
|
|
485
|
+
elif args.command == "remote-signature-resolve":
|
|
486
|
+
print_json(
|
|
487
|
+
client.resolve_remote_signature(
|
|
488
|
+
parse_json_arg(args.ref, dict),
|
|
489
|
+
force=args.force,
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
elif args.command == "object-add":
|
|
493
|
+
print_json(
|
|
494
|
+
client.register_object(
|
|
495
|
+
args.name,
|
|
496
|
+
entrypoint=args.entrypoint,
|
|
497
|
+
env=args.env,
|
|
498
|
+
yaml_path=args.yaml_path,
|
|
499
|
+
workdir=args.workdir,
|
|
500
|
+
runtime_config=read_runtime_config(
|
|
501
|
+
args.runtime_config,
|
|
502
|
+
runtime=args.runtime,
|
|
503
|
+
python=args.python_version,
|
|
504
|
+
base_image=args.base_image,
|
|
505
|
+
),
|
|
506
|
+
description=args.description,
|
|
507
|
+
version_label=args.version_label,
|
|
508
|
+
object_id=args.object_id,
|
|
509
|
+
local_only=args.local_only,
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
elif args.command == "object-list":
|
|
513
|
+
print_json(client.list_objects(query=args.query, compact=args.compact))
|
|
514
|
+
elif args.command == "object-show":
|
|
515
|
+
print_json(client.get_object(args.name_or_id, version=args.version))
|
|
516
|
+
elif args.command == "object-signature":
|
|
517
|
+
print_json(client.signature(args.name_or_id, version=args.version))
|
|
518
|
+
elif args.command == "object-inputs":
|
|
519
|
+
print_json(client.inputs(args.name_or_id, version=args.version))
|
|
520
|
+
elif args.command == "object-outputs":
|
|
521
|
+
print_json(client.outputs(args.name_or_id, version=args.version))
|
|
522
|
+
elif args.command == "object-versions":
|
|
523
|
+
print_json(client.object_versions(args.name_or_id))
|
|
524
|
+
elif args.command == "run":
|
|
525
|
+
positional_args = parse_json_arg(args.args, list)
|
|
526
|
+
keyword_args = parse_json_arg(args.kwargs, dict)
|
|
527
|
+
state = client.run(
|
|
528
|
+
args.object,
|
|
529
|
+
args=positional_args,
|
|
530
|
+
kwargs=keyword_args,
|
|
531
|
+
output=args.output,
|
|
532
|
+
timeout_seconds=args.timeout,
|
|
533
|
+
version=args.version,
|
|
534
|
+
version_id=args.version_id,
|
|
535
|
+
target_machine=args.target_machine,
|
|
536
|
+
source=args.source,
|
|
537
|
+
)
|
|
538
|
+
if not args.wait:
|
|
539
|
+
print_json(state)
|
|
540
|
+
return 0
|
|
541
|
+
|
|
542
|
+
if args.target_machine is not None:
|
|
543
|
+
final_state = client.wait_remote_run(
|
|
544
|
+
state["id"],
|
|
545
|
+
timeout_seconds=args.timeout,
|
|
546
|
+
)
|
|
547
|
+
print_json(
|
|
548
|
+
{
|
|
549
|
+
"run": final_state,
|
|
550
|
+
"payload": final_state.get("result") or {},
|
|
551
|
+
}
|
|
552
|
+
)
|
|
553
|
+
return 0
|
|
554
|
+
|
|
555
|
+
final_state = client.wait_run(state["id"])
|
|
556
|
+
if final_state["status"] == "succeeded":
|
|
557
|
+
print_json(
|
|
558
|
+
{
|
|
559
|
+
"run": final_state,
|
|
560
|
+
"payload": client.result(final_state["id"]),
|
|
561
|
+
}
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
print_json({"run": final_state})
|
|
565
|
+
elif args.command == "run-status":
|
|
566
|
+
print_json(client.get_run(args.run_id))
|
|
567
|
+
elif args.command == "run-list":
|
|
568
|
+
print_json(client.list_runs())
|
|
569
|
+
elif args.command == "run-result":
|
|
570
|
+
print_json(client.result(args.run_id))
|
|
571
|
+
elif args.command == "artifact-list":
|
|
572
|
+
print_json(client.list_artifacts(args.run_id))
|
|
573
|
+
elif args.command == "artifact-get":
|
|
574
|
+
path = client.download_artifact(args.run_id, args.name, args.target)
|
|
575
|
+
print_json({"path": str(path)})
|
|
576
|
+
else:
|
|
577
|
+
raise AssertionError(f"unhandled command: {args.command}")
|
|
578
|
+
except Exception as exc: # noqa: BLE001 - CLI should show concise errors.
|
|
579
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
580
|
+
return 1
|
|
581
|
+
|
|
582
|
+
return 0
|
spl/daemon/client.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Compatibility imports for the framework-side local daemon HTTP client."""
|
|
2
|
+
|
|
3
|
+
from spl.daemon_client import (
|
|
4
|
+
DAEMON_ENDPOINT_FILENAME,
|
|
5
|
+
DAEMON_API_TOKEN_ENV,
|
|
6
|
+
DEFAULT_DAEMON_HOST,
|
|
7
|
+
DEFAULT_DAEMON_PORT,
|
|
8
|
+
DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
|
|
9
|
+
DEFAULT_SERVER_URL,
|
|
10
|
+
DEFAULT_URL,
|
|
11
|
+
Client,
|
|
12
|
+
ClientError,
|
|
13
|
+
clear_daemon_endpoint,
|
|
14
|
+
daemon_endpoint_file,
|
|
15
|
+
daemon_url,
|
|
16
|
+
default_daemon_home,
|
|
17
|
+
generate_daemon_api_token,
|
|
18
|
+
read_daemon_endpoint,
|
|
19
|
+
resolve_api_token,
|
|
20
|
+
resolve_base_url,
|
|
21
|
+
write_daemon_endpoint,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DAEMON_ENDPOINT_FILENAME",
|
|
26
|
+
"DAEMON_API_TOKEN_ENV",
|
|
27
|
+
"DEFAULT_DAEMON_HOST",
|
|
28
|
+
"DEFAULT_DAEMON_PORT",
|
|
29
|
+
"DEFAULT_HEARTBEAT_INTERVAL_SECONDS",
|
|
30
|
+
"DEFAULT_SERVER_URL",
|
|
31
|
+
"DEFAULT_URL",
|
|
32
|
+
"Client",
|
|
33
|
+
"ClientError",
|
|
34
|
+
"clear_daemon_endpoint",
|
|
35
|
+
"daemon_endpoint_file",
|
|
36
|
+
"daemon_url",
|
|
37
|
+
"default_daemon_home",
|
|
38
|
+
"generate_daemon_api_token",
|
|
39
|
+
"read_daemon_endpoint",
|
|
40
|
+
"resolve_api_token",
|
|
41
|
+
"resolve_base_url",
|
|
42
|
+
"write_daemon_endpoint",
|
|
43
|
+
]
|