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.
Files changed (74) hide show
  1. spl/__init__.py +14 -0
  2. spl/client.py +1364 -0
  3. spl/core/__init__.py +23 -0
  4. spl/core/common.py +350 -0
  5. spl/core/entities/__init__.py +0 -0
  6. spl/core/entities/adapter.py +210 -0
  7. spl/core/entities/artifact.py +141 -0
  8. spl/core/entities/control.py +45 -0
  9. spl/core/entities/distribution.py +65 -0
  10. spl/core/entities/function.py +254 -0
  11. spl/core/entities/local_function.py +286 -0
  12. spl/core/entities/misc.py +14 -0
  13. spl/core/entities/module.py +88 -0
  14. spl/core/entities/node.py +286 -0
  15. spl/core/entities/node_function.py +79 -0
  16. spl/core/entities/node_remote.py +295 -0
  17. spl/core/entities/pipeline.py +436 -0
  18. spl/core/entities/scalar.py +55 -0
  19. spl/core/ir/__init__.py +0 -0
  20. spl/core/ir/common.py +34 -0
  21. spl/core/ir/parse.py +79 -0
  22. spl/core/ir/unparse.py +29 -0
  23. spl/core/ir/utils.py +163 -0
  24. spl/daemon/__init__.py +23 -0
  25. spl/daemon/__main__.py +11 -0
  26. spl/daemon/cli.py +582 -0
  27. spl/daemon/client.py +43 -0
  28. spl/daemon/docker_environment.py +329 -0
  29. spl/daemon/docker_pool.py +516 -0
  30. spl/daemon/environment.py +228 -0
  31. spl/daemon/environment_base.py +479 -0
  32. spl/daemon/heartbeat_service.py +119 -0
  33. spl/daemon/metadata.py +427 -0
  34. spl/daemon/remote_client.py +457 -0
  35. spl/daemon/repositories/__init__.py +17 -0
  36. spl/daemon/repositories/env.py +323 -0
  37. spl/daemon/repositories/library.py +181 -0
  38. spl/daemon/repositories/object.py +997 -0
  39. spl/daemon/repositories/run.py +279 -0
  40. spl/daemon/repositories/server_connection.py +657 -0
  41. spl/daemon/repositories/sync_event.py +129 -0
  42. spl/daemon/routes/__init__.py +1 -0
  43. spl/daemon/routes/_helpers.py +147 -0
  44. spl/daemon/routes/artifacts.py +77 -0
  45. spl/daemon/routes/diagnostics.py +114 -0
  46. spl/daemon/routes/envs.py +82 -0
  47. spl/daemon/routes/libraries.py +129 -0
  48. spl/daemon/routes/objects.py +174 -0
  49. spl/daemon/routes/remote.py +56 -0
  50. spl/daemon/routes/runs.py +96 -0
  51. spl/daemon/routes/server_connections.py +86 -0
  52. spl/daemon/runtime_backend.py +368 -0
  53. spl/daemon/runtime_config.py +133 -0
  54. spl/daemon/runtime_dependencies.py +459 -0
  55. spl/daemon/secret_store.py +187 -0
  56. spl/daemon/server.py +2224 -0
  57. spl/daemon/server_connection.py +267 -0
  58. spl/daemon/services/__init__.py +1 -0
  59. spl/daemon/services/sync.py +76 -0
  60. spl/daemon/signature.py +376 -0
  61. spl/daemon/storage_base.py +542 -0
  62. spl/daemon/store.py +436 -0
  63. spl/daemon/worker.py +526 -0
  64. spl/daemon_client.py +945 -0
  65. spl/pipeline_widget.py +1452 -0
  66. spl/py.typed +0 -0
  67. spl/server_client.py +787 -0
  68. splime-0.1.2.dist-info/METADATA +189 -0
  69. splime-0.1.2.dist-info/RECORD +74 -0
  70. splime-0.1.2.dist-info/WHEEL +5 -0
  71. splime-0.1.2.dist-info/entry_points.txt +2 -0
  72. splime-0.1.2.dist-info/licenses/LICENSE +201 -0
  73. splime-0.1.2.dist-info/licenses/NOTICE +8 -0
  74. 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
+ ]