splime 0.2.5__tar.gz → 0.4.0__tar.gz

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 (115) hide show
  1. {splime-0.2.5/src/splime.egg-info → splime-0.4.0}/PKG-INFO +3 -1
  2. {splime-0.2.5 → splime-0.4.0}/pyproject.toml +8 -3
  3. {splime-0.2.5 → splime-0.4.0}/src/spl/_client.py +181 -140
  4. {splime-0.2.5 → splime-0.4.0}/src/spl/_http.py +9 -2
  5. {splime-0.2.5 → splime-0.4.0}/src/spl/_views.py +94 -65
  6. {splime-0.2.5 → splime-0.4.0}/src/spl/core/__init__.py +1 -6
  7. splime-0.4.0/src/spl/core/_common.py +1263 -0
  8. splime-0.4.0/src/spl/core/adapter_compat.py +459 -0
  9. splime-0.4.0/src/spl/core/entities/adapter.py +426 -0
  10. splime-0.4.0/src/spl/core/entities/artifact.py +136 -0
  11. splime-0.4.0/src/spl/core/entities/control.py +44 -0
  12. splime-0.4.0/src/spl/core/entities/distribution.py +56 -0
  13. splime-0.4.0/src/spl/core/entities/function.py +263 -0
  14. {splime-0.2.5 → splime-0.4.0}/src/spl/core/entities/local_function.py +27 -33
  15. {splime-0.2.5 → splime-0.4.0}/src/spl/core/entities/misc.py +3 -3
  16. splime-0.4.0/src/spl/core/entities/module.py +82 -0
  17. splime-0.4.0/src/spl/core/entities/node.py +289 -0
  18. splime-0.4.0/src/spl/core/entities/node_function.py +71 -0
  19. splime-0.4.0/src/spl/core/entities/node_remote.py +298 -0
  20. splime-0.4.0/src/spl/core/entities/pipeline.py +526 -0
  21. splime-0.4.0/src/spl/core/entities/scalar.py +48 -0
  22. splime-0.4.0/src/spl/core/fingerprint.py +201 -0
  23. splime-0.4.0/src/spl/core/ir/common.py +42 -0
  24. {splime-0.2.5 → splime-0.4.0}/src/spl/core/ir/parse.py +18 -18
  25. splime-0.4.0/src/spl/core/ir/unparse.py +38 -0
  26. splime-0.4.0/src/spl/core/ir/utils.py +159 -0
  27. splime-0.4.0/src/spl/core/manifest.py +1003 -0
  28. splime-0.4.0/src/spl/core/node_runtime.py +524 -0
  29. splime-0.4.0/src/spl/core/resume.py +324 -0
  30. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/canonical.py +39 -22
  31. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/cli.py +93 -19
  32. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/docker_environment.py +6 -21
  33. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/docker_pool.py +5 -20
  34. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/doctor.py +138 -12
  35. splime-0.4.0/src/spl/daemon/environment.py +429 -0
  36. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/environment_base.py +6 -19
  37. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/heartbeat_service.py +1 -5
  38. splime-0.4.0/src/spl/daemon/interpreter_visibility.py +69 -0
  39. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/metadata.py +12 -37
  40. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/remote_client.py +129 -117
  41. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/env.py +27 -20
  42. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/library.py +3 -12
  43. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/object.py +142 -176
  44. splime-0.4.0/src/spl/daemon/repositories/run.py +580 -0
  45. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/server_connection.py +11 -25
  46. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/_helpers.py +38 -20
  47. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/artifacts.py +5 -13
  48. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/diagnostics.py +57 -11
  49. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/envs.py +3 -5
  50. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/libraries.py +2 -2
  51. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/objects.py +12 -21
  52. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/remote.py +2 -2
  53. splime-0.4.0/src/spl/daemon/routes/runs.py +174 -0
  54. splime-0.4.0/src/spl/daemon/routes/server_connections.py +155 -0
  55. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/run_progress.py +41 -0
  56. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/runtime_backend.py +85 -12
  57. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/runtime_config.py +3 -12
  58. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/runtime_dependencies.py +17 -0
  59. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/secret_store.py +3 -6
  60. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/server.py +348 -176
  61. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/server_connection.py +52 -7
  62. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/services/sync.py +3 -14
  63. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/signature.py +15 -38
  64. splime-0.4.0/src/spl/daemon/spl_free_generator.py +241 -0
  65. splime-0.4.0/src/spl/daemon/spl_free_runner.py +252 -0
  66. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/storage_base.py +87 -105
  67. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/store.py +38 -3
  68. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/worker.py +326 -111
  69. splime-0.4.0/src/spl/daemon/worker_runtime_marker.py +3 -0
  70. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon_client.py +340 -121
  71. {splime-0.2.5 → splime-0.4.0}/src/spl/pipeline_widget.py +45 -38
  72. {splime-0.2.5 → splime-0.4.0}/src/spl/server_client.py +17 -33
  73. {splime-0.2.5 → splime-0.4.0/src/splime.egg-info}/PKG-INFO +3 -1
  74. {splime-0.2.5 → splime-0.4.0}/src/splime.egg-info/SOURCES.txt +9 -0
  75. {splime-0.2.5 → splime-0.4.0}/src/splime.egg-info/requires.txt +2 -0
  76. splime-0.2.5/src/spl/core/_common.py +0 -375
  77. splime-0.2.5/src/spl/core/entities/adapter.py +0 -210
  78. splime-0.2.5/src/spl/core/entities/artifact.py +0 -140
  79. splime-0.2.5/src/spl/core/entities/control.py +0 -45
  80. splime-0.2.5/src/spl/core/entities/distribution.py +0 -65
  81. splime-0.2.5/src/spl/core/entities/function.py +0 -257
  82. splime-0.2.5/src/spl/core/entities/module.py +0 -88
  83. splime-0.2.5/src/spl/core/entities/node.py +0 -286
  84. splime-0.2.5/src/spl/core/entities/node_function.py +0 -79
  85. splime-0.2.5/src/spl/core/entities/node_remote.py +0 -349
  86. splime-0.2.5/src/spl/core/entities/pipeline.py +0 -436
  87. splime-0.2.5/src/spl/core/entities/scalar.py +0 -55
  88. splime-0.2.5/src/spl/core/ir/common.py +0 -34
  89. splime-0.2.5/src/spl/core/ir/unparse.py +0 -29
  90. splime-0.2.5/src/spl/core/ir/utils.py +0 -163
  91. splime-0.2.5/src/spl/daemon/environment.py +0 -269
  92. splime-0.2.5/src/spl/daemon/repositories/run.py +0 -271
  93. splime-0.2.5/src/spl/daemon/routes/runs.py +0 -101
  94. splime-0.2.5/src/spl/daemon/routes/server_connections.py +0 -86
  95. {splime-0.2.5 → splime-0.4.0}/LICENSE +0 -0
  96. {splime-0.2.5 → splime-0.4.0}/NOTICE +0 -0
  97. {splime-0.2.5 → splime-0.4.0}/README.md +0 -0
  98. {splime-0.2.5 → splime-0.4.0}/setup.cfg +0 -0
  99. {splime-0.2.5 → splime-0.4.0}/src/spl/__init__.py +0 -0
  100. {splime-0.2.5 → splime-0.4.0}/src/spl/_deprecate.py +0 -0
  101. {splime-0.2.5 → splime-0.4.0}/src/spl/client.py +0 -0
  102. {splime-0.2.5 → splime-0.4.0}/src/spl/core/common.py +0 -0
  103. {splime-0.2.5 → splime-0.4.0}/src/spl/core/entities/__init__.py +0 -0
  104. {splime-0.2.5 → splime-0.4.0}/src/spl/core/ir/__init__.py +0 -0
  105. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/__init__.py +0 -0
  106. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/__main__.py +0 -0
  107. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/client.py +0 -0
  108. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/__init__.py +0 -0
  109. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/repositories/sync_event.py +0 -0
  110. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/routes/__init__.py +0 -0
  111. {splime-0.2.5 → splime-0.4.0}/src/spl/daemon/services/__init__.py +0 -0
  112. {splime-0.2.5 → splime-0.4.0}/src/spl/py.typed +0 -0
  113. {splime-0.2.5 → splime-0.4.0}/src/splime.egg-info/dependency_links.txt +0 -0
  114. {splime-0.2.5 → splime-0.4.0}/src/splime.egg-info/entry_points.txt +0 -0
  115. {splime-0.2.5 → splime-0.4.0}/src/splime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splime
3
- Version: 0.2.5
3
+ Version: 0.4.0
4
4
  Summary: Reuse Python functions across projects without rewriting or redeploying them.
5
5
  Author-email: Yastrebov Kirill <yastrebovks@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -42,6 +42,8 @@ Requires-Dist: pyexpect; extra == "test"
42
42
  Requires-Dist: mypy==2.1.0; extra == "test"
43
43
  Requires-Dist: pytest==9.1.1; extra == "test"
44
44
  Requires-Dist: pytest-asyncio; extra == "test"
45
+ Requires-Dist: ruff==0.15.20; extra == "test"
46
+ Requires-Dist: types-PyYAML==6.0.12.20260518; extra == "test"
45
47
  Provides-Extra: docs
46
48
  Requires-Dist: sphinx==9.1.0; extra == "docs"
47
49
  Requires-Dist: sphinx-rtd-theme==3.1.0; extra == "docs"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splime"
7
- version = "0.2.5"
7
+ version = "0.4.0"
8
8
 
9
9
  description = "Reuse Python functions across projects without rewriting or redeploying them."
10
10
  readme = "README.md"
@@ -55,7 +55,9 @@ test = [
55
55
  "pyexpect",
56
56
  "mypy==2.1.0",
57
57
  "pytest==9.1.1",
58
- "pytest-asyncio"]
58
+ "pytest-asyncio",
59
+ "ruff==0.15.20",
60
+ "types-PyYAML==6.0.12.20260518"]
59
61
 
60
62
  docs = [
61
63
  "sphinx==9.1.0",
@@ -105,7 +107,7 @@ strict = true
105
107
  line-length = 120
106
108
 
107
109
  [tool.ruff.lint]
108
- select = []
110
+ select = ["E4", "E7", "E9", "F"]
109
111
  ignore = []
110
112
 
111
113
  # Scoped, justified allowances. These silence rules only where the pattern is
@@ -125,6 +127,9 @@ ignore = []
125
127
  "S603", "S607", "S310", "S104", "S108", "S608",
126
128
  "S105", "B904", "S110", "S112", "N818",
127
129
  ]
130
+ # The legacy worker must repair sys.path before importing packages that can be
131
+ # shadowed by the daemon checkout injected through PYTHONPATH.
132
+ "src/spl/daemon/worker.py" = ["E402"]
128
133
  # The CLI and daemon bootstrap print to the console on purpose.
129
134
  "src/spl/daemon/cli.py" = ["T201"]
130
135
  "src/spl/daemon/server.py" = ["T201"]
@@ -18,6 +18,7 @@ dependencies imported yet.
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import builtins
21
22
  from dataclasses import dataclass, field, replace
22
23
  from html import escape
23
24
  from pathlib import Path
@@ -38,6 +39,7 @@ from spl._views import (
38
39
  SignatureView,
39
40
  wrap_action,
40
41
  )
42
+ from spl.core import manifest as m_manifest
41
43
  from spl.core.entities.node import DEFAULT_PORT
42
44
  from spl.daemon_client import (
43
45
  DEFAULT_DAEMON_HOST,
@@ -70,10 +72,8 @@ def _preview(value: Any, *, limit: int = 80) -> str:
70
72
 
71
73
 
72
74
  def _is_missing_server_connection(exc: Exception) -> bool:
73
- return (
74
- isinstance(exc, ClientError)
75
- and _NO_SERVER_CONNECTION_MESSAGE in str(exc)
76
- )
75
+ message = str(exc)
76
+ return isinstance(exc, ClientError) and _NO_SERVER_CONNECTION_MESSAGE in message
77
77
 
78
78
 
79
79
  def _progress_callback(progress: ProgressOption) -> RunStateCallback | None:
@@ -141,10 +141,7 @@ class PublishedObject:
141
141
  "entrypoint": self.entrypoint,
142
142
  }
143
143
  body = "".join(
144
- "<tr>"
145
- f"<th style='text-align:left'>{escape(key)}</th>"
146
- f"<td><code>{escape(value)}</code></td>"
147
- "</tr>"
144
+ f"<tr><th style='text-align:left'>{escape(key)}</th><td><code>{escape(value)}</code></td></tr>"
148
145
  for key, value in rows.items()
149
146
  )
150
147
  return f"<table><tbody>{body}</tbody></table>"
@@ -170,11 +167,7 @@ def _catalog_rows(
170
167
  rows: list[dict[str, str]] = []
171
168
  for record in records:
172
169
  library = record.get("library")
173
- library_name = (
174
- library.get("display_name") or library.get("slug")
175
- if isinstance(library, dict)
176
- else library
177
- )
170
+ library_name = library.get("display_name") or library.get("slug") if isinstance(library, dict) else library
178
171
  version_value = record.get("version")
179
172
  if version_value is None:
180
173
  current = record.get("current_version")
@@ -197,28 +190,16 @@ def _catalog_rows(
197
190
  def _rows_to_text(rows: list[dict[str, str]], title: str) -> str:
198
191
  if not rows:
199
192
  return f"{title}: (empty)"
200
- widths = {
201
- header: max(len(header), *(len(row[header]) for row in rows))
202
- for header in _CATALOG_HEADERS
203
- }
193
+ widths = {header: max(len(header), *(len(row[header]) for row in rows)) for header in _CATALOG_HEADERS}
204
194
  head = " ".join(header.ljust(widths[header]) for header in _CATALOG_HEADERS)
205
- body = "\n".join(
206
- " ".join(row[header].ljust(widths[header]) for header in _CATALOG_HEADERS)
207
- for row in rows
208
- )
195
+ body = "\n".join(" ".join(row[header].ljust(widths[header]) for header in _CATALOG_HEADERS) for row in rows)
209
196
  return f"{title} ({len(rows)}):\n{head}\n{body}"
210
197
 
211
198
 
212
199
  def _rows_to_html(rows: list[dict[str, str]], title: str) -> str:
213
- head = "".join(
214
- f"<th style='text-align:left'>{escape(header)}</th>"
215
- for header in _CATALOG_HEADERS
216
- )
200
+ head = "".join(f"<th style='text-align:left'>{escape(header)}</th>" for header in _CATALOG_HEADERS)
217
201
  body = "".join(
218
- "<tr>"
219
- + "".join(f"<td>{escape(row[header])}</td>" for header in _CATALOG_HEADERS)
220
- + "</tr>"
221
- for row in rows
202
+ "<tr>" + "".join(f"<td>{escape(row[header])}</td>" for header in _CATALOG_HEADERS) + "</tr>" for row in rows
222
203
  )
223
204
  return (
224
205
  f"<div><b>{escape(title)}</b> ({len(rows)})"
@@ -258,14 +239,10 @@ class ObjectCatalog(dict[str, Any]):
258
239
  """Local+server catalog that prints per-scope tables; ``.raw`` is a plain dict."""
259
240
 
260
241
  def __repr__(self) -> str:
261
- return "\n\n".join(
262
- _rows_to_text(_catalog_rows(value), str(key)) for key, value in self.items()
263
- )
242
+ return "\n\n".join(_rows_to_text(_catalog_rows(value), str(key)) for key, value in self.items())
264
243
 
265
244
  def _repr_html_(self) -> str:
266
- return "".join(
267
- _rows_to_html(_catalog_rows(value), str(key)) for key, value in self.items()
268
- )
245
+ return "".join(_rows_to_html(_catalog_rows(value), str(key)) for key, value in self.items())
269
246
 
270
247
  @property
271
248
  def raw(self) -> dict[str, Any]:
@@ -343,10 +320,7 @@ class RemoteResult:
343
320
  "artifacts": str(len(self.artifacts)),
344
321
  }
345
322
  body = "".join(
346
- "<tr>"
347
- f"<th style='text-align:left'>{escape(key)}</th>"
348
- f"<td><code>{escape(value)}</code></td>"
349
- "</tr>"
323
+ f"<tr><th style='text-align:left'>{escape(key)}</th><td><code>{escape(value)}</code></td></tr>"
350
324
  for key, value in rows.items()
351
325
  )
352
326
  return f"<table><tbody>{body}</tbody></table>"
@@ -375,13 +349,13 @@ class RemoteRun:
375
349
  def id(self) -> str:
376
350
  """Return the daemon run id."""
377
351
 
378
- return self.state["id"]
352
+ return cast(str, self.state["id"])
379
353
 
380
354
  @property
381
355
  def status(self) -> str:
382
356
  """Return the last known daemon status."""
383
357
 
384
- return self.state["status"]
358
+ return cast(str, self.state["status"])
385
359
 
386
360
  @property
387
361
  def mode(self) -> str:
@@ -490,17 +464,10 @@ class RemoteRun:
490
464
  )
491
465
  if final_state["status"] != "succeeded":
492
466
  error = final_state.get("error") or "run returned no error message"
493
- raise RuntimeError(
494
- f"{self.mode} run {self.id!r} ended as "
495
- f"{final_state.get('status')!r}: {error}"
496
- )
467
+ raise RuntimeError(f"{self.mode} run {self.id!r} ended as {final_state.get('status')!r}: {error}")
497
468
 
498
469
  payload = self.result()
499
- downloaded = (
500
- self.download_artifacts(artifacts_dir)
501
- if artifacts_dir is not None
502
- else {}
503
- )
470
+ downloaded = self.download_artifacts(artifacts_dir) if artifacts_dir is not None else {}
504
471
  return RemoteResult(
505
472
  run=final_state,
506
473
  payload=payload,
@@ -593,7 +560,7 @@ class _LibraryAdmin:
593
560
  grantee: str,
594
561
  *,
595
562
  grantee_type: str = "user",
596
- scopes: list[str] | None = None,
563
+ scopes: builtins.list[str] | None = None,
597
564
  ) -> dict[str, Any]:
598
565
  """Grant a user or team access to one central-server library."""
599
566
 
@@ -779,11 +746,7 @@ class SPLClient:
779
746
  )
780
747
  conn = self.current_server_connection()
781
748
  connection = conn.get("connection")
782
- nested_url = (
783
- connection.get("server_url")
784
- if isinstance(connection, dict)
785
- else None
786
- )
749
+ nested_url = connection.get("server_url") if isinstance(connection, dict) else None
787
750
  server_url = conn.get("server_url") or nested_url or DEFAULT_SERVER_URL
788
751
  return SPLServerClient(token=self._user_token, base_url=server_url)
789
752
 
@@ -800,28 +763,42 @@ class SPLClient:
800
763
  state = self._daemon.server_connection()
801
764
  except Exception as exc:
802
765
  if _is_missing_server_connection(exc):
803
- return ConnectionStatusView(
804
- {"connected": False, "offline": False, "connection": None}
805
- )
766
+ return ConnectionStatusView({"connected": False, "offline": False, "connection": None})
806
767
  raise
807
768
  state.setdefault("connected", bool(state.get("server_url")))
808
769
  return ConnectionStatusView(state)
809
770
 
810
771
  def machines(self) -> dict[str, Any]:
811
- """Return the user's machines, or an empty listing when not connected."""
772
+ """Return the user's machines, or an empty listing when not connected.
812
773
 
813
- if not self._has_server_connection():
774
+ The connection-state endpoint is probed for health only: a missing
775
+ connection maps to an empty listing and any other state failure
776
+ re-raises (0.3.0 contract). A degraded-but-present state (for
777
+ example ``heartbeat_failed``) must not hide the daemon's machine
778
+ payload, so the state content itself does not gate the listing.
779
+ """
780
+
781
+ if self.server_connection is None:
782
+ try:
783
+ self._daemon.server_connection()
784
+ except Exception as exc:
785
+ if not _is_missing_server_connection(exc):
786
+ raise
787
+ return MachineListView({"current_machine_id": None, "machines": []})
788
+ try:
789
+ machines = self._daemon.server_machines()
790
+ except Exception as exc:
791
+ if not _is_missing_server_connection(exc):
792
+ raise
814
793
  return MachineListView({"current_machine_id": None, "machines": []})
815
- return MachineListView(self._daemon.server_machines())
794
+ return MachineListView(machines)
816
795
 
817
796
  def libraries(self, *, include_accessible: bool = True) -> list[dict[str, Any]]:
818
797
  """Return visible libraries, or an empty list when not connected."""
819
798
 
820
799
  if not self._has_server_connection():
821
800
  return LibraryListView([])
822
- return LibraryListView(
823
- self._daemon.server_libraries(include_accessible=include_accessible)
824
- )
801
+ return LibraryListView(self._daemon.server_libraries(include_accessible=include_accessible))
825
802
 
826
803
  # The flat library aliases (create_library, get_library, update_library,
827
804
  # delete_library, grant_library, revoke_library_grant, add_reference,
@@ -1018,11 +995,7 @@ class SPLClient:
1018
995
  """
1019
996
 
1020
997
  if scope == "auto":
1021
- scope = (
1022
- "server"
1023
- if owner is not None or library is not None or self._has_server_connection()
1024
- else "local"
1025
- )
998
+ scope = "server" if owner is not None or library is not None or self._has_server_connection() else "local"
1026
999
  if scope == "local":
1027
1000
  if owner is not None or library is not None:
1028
1001
  raise ValueError("owner/library require scope='server', scope='all', or scope='auto'")
@@ -1220,9 +1193,7 @@ class SPLClient:
1220
1193
  """Return normalized function/node/link metadata for one object."""
1221
1194
 
1222
1195
  if self._is_node_remote(name):
1223
- return DecompositionView(
1224
- self._remote_decomposition_response(name, version=version)["decomposition"]
1225
- )
1196
+ return DecompositionView(self._remote_decomposition_response(name, version=version)["decomposition"])
1226
1197
  if owner is not None or library is not None:
1227
1198
  response = self._remote_decomposition_response(
1228
1199
  {
@@ -1269,11 +1240,7 @@ class SPLClient:
1269
1240
  record = response.get("object") or {}
1270
1241
  remote = response.get("remote") or {}
1271
1242
  object_name = (
1272
- title
1273
- or record.get("display_name")
1274
- or record.get("name")
1275
- or remote.get("name")
1276
- or pipeline.name
1243
+ title or record.get("display_name") or record.get("name") or remote.get("name") or pipeline.name
1277
1244
  )
1278
1245
  return PipelineGraphWidget(
1279
1246
  decomposition,
@@ -1328,10 +1295,7 @@ class SPLClient:
1328
1295
  theme=theme,
1329
1296
  )
1330
1297
 
1331
- raise TypeError(
1332
- "pipeline_widget expects an object name, PublishedObject, "
1333
- "spl.core Pipeline, or NodeRemote"
1334
- )
1298
+ raise TypeError("pipeline_widget expects an object name, PublishedObject, spl.core Pipeline, or NodeRemote")
1335
1299
 
1336
1300
  def draw_pipeline(
1337
1301
  self,
@@ -1371,20 +1335,11 @@ class SPLClient:
1371
1335
  function=function,
1372
1336
  )
1373
1337
  display_name = signature.get("display_name") or signature["name"]
1374
- lines = [
1375
- (
1376
- f"{display_name} "
1377
- f"v{signature['version']} ({signature['kind']})"
1378
- )
1379
- ]
1338
+ lines = [(f"{display_name} v{signature['version']} ({signature['kind']})")]
1380
1339
  if signature.get("description"):
1381
1340
  lines.append(signature["description"])
1382
1341
 
1383
- if (
1384
- function is None
1385
- and signature.get("kind") == "pipeline"
1386
- and signature.get("internal_functions")
1387
- ):
1342
+ if function is None and signature.get("kind") == "pipeline" and signature.get("internal_functions"):
1388
1343
  lines.append("Functions:")
1389
1344
  for item in signature["internal_functions"]:
1390
1345
  lines.append(f" - {item['name']}")
@@ -1393,29 +1348,16 @@ class SPLClient:
1393
1348
  if signature["inputs"]:
1394
1349
  for item in signature["inputs"]:
1395
1350
  required = "required" if item["required"] else "optional"
1396
- default = (
1397
- ""
1398
- if item["default"] is None
1399
- else f", default={item['default']}"
1400
- )
1401
- lines.append(
1402
- f" - {item['name']}: {item['type'] or 'Any'} "
1403
- f"({required}{default})"
1404
- )
1351
+ default = "" if item["default"] is None else f", default={item['default']}"
1352
+ lines.append(f" - {item['name']}: {item['type'] or 'Any'} ({required}{default})")
1405
1353
  else:
1406
1354
  lines.append(" - none")
1407
1355
 
1408
1356
  lines.append("Outputs:")
1409
1357
  if signature["outputs"]:
1410
1358
  for item in signature["outputs"]:
1411
- selector = (
1412
- f'output="{item["selector"]}"'
1413
- if item["selector"] is not None
1414
- else "no output selector"
1415
- )
1416
- lines.append(
1417
- f" - {item['name']}: {selector}; read {item['read']}"
1418
- )
1359
+ selector = f'output="{item["selector"]}"' if item["selector"] is not None else "no output selector"
1360
+ lines.append(f" - {item['name']}: {selector}; read {item['read']}")
1419
1361
  else:
1420
1362
  lines.append(" - none")
1421
1363
 
@@ -1446,10 +1388,89 @@ class SPLClient:
1446
1388
  "environment build",
1447
1389
  )
1448
1390
 
1449
- def runs(self) -> list[dict[str, Any]]:
1391
+ def runs(self, *, local: bool = False) -> list[dict[str, Any]]:
1450
1392
  """Return known daemon runs, newest first."""
1451
1393
 
1452
- return RunListView(self._daemon.list_runs())
1394
+ return RunListView(m_manifest.list_local_runs() if local else self._daemon.list_runs())
1395
+
1396
+ def run_show(self, run_id: str, *, full_inline: bool = False, local: bool = False) -> dict[str, Any]:
1397
+ """Return one retained run manifest.
1398
+
1399
+ Inline JSON values are summarized by default; pass ``full_inline=True``
1400
+ to include the full manifest values.
1401
+ """
1402
+
1403
+ payload = (
1404
+ m_manifest.show_local_run(run_id, include_inline_values=full_inline)
1405
+ if local
1406
+ else self._daemon.show_run(run_id, full_inline=full_inline)
1407
+ )
1408
+ return RunRecordView(payload)
1409
+
1410
+ def resume(
1411
+ self,
1412
+ run_id: str,
1413
+ *,
1414
+ from_: Any,
1415
+ kwargs: dict[str, Any] | None = None,
1416
+ output: str | None = None,
1417
+ timeout_seconds: float | None = None,
1418
+ adapters: dict[str, Any] | None = None,
1419
+ runtimes: dict[str, str] | None = None,
1420
+ keep: bool | str | None = None,
1421
+ wait: bool = False,
1422
+ artifacts_dir: str | Path | None = None,
1423
+ progress: ProgressOption = True,
1424
+ ) -> RemoteRun | RemoteResult:
1425
+ """Resume a retained daemon pipeline run from selected recalculation nodes."""
1426
+
1427
+ state = self._daemon.resume_run(
1428
+ run_id,
1429
+ from_=from_,
1430
+ kwargs=kwargs,
1431
+ output=output,
1432
+ timeout_seconds=timeout_seconds,
1433
+ adapters=adapters,
1434
+ runtimes=runtimes,
1435
+ keep=keep,
1436
+ )
1437
+ run = RemoteRun(self, state)
1438
+ if not wait:
1439
+ return run
1440
+ return run.collect(
1441
+ artifacts_dir=artifacts_dir,
1442
+ timeout_seconds=timeout_seconds,
1443
+ progress=progress,
1444
+ )
1445
+
1446
+ def prune_runs(
1447
+ self,
1448
+ *,
1449
+ run_id: str | None = None,
1450
+ status: str | list[str] | None = None,
1451
+ older_than_seconds: float | None = None,
1452
+ dry_run: bool = False,
1453
+ local: bool = False,
1454
+ ) -> dict[str, Any]:
1455
+ """Prune inactive retained runs by id, status, age, or retention TTL."""
1456
+
1457
+ statuses = [status] if isinstance(status, str) else status
1458
+ result = (
1459
+ m_manifest.prune_local_runs(
1460
+ run_id=run_id,
1461
+ statuses=statuses,
1462
+ older_than_seconds=older_than_seconds,
1463
+ dry_run=dry_run,
1464
+ )
1465
+ if local
1466
+ else self._daemon.prune_runs(
1467
+ run_id=run_id,
1468
+ statuses=statuses,
1469
+ older_than_seconds=older_than_seconds,
1470
+ dry_run=dry_run,
1471
+ )
1472
+ )
1473
+ return wrap_action(result, "runs pruned")
1453
1474
 
1454
1475
  def _start_run(
1455
1476
  self,
@@ -1465,24 +1486,37 @@ class SPLClient:
1465
1486
  offline_policy: OfflinePolicy | None = None,
1466
1487
  function: str | None = None,
1467
1488
  source: RunSource = "auto",
1489
+ adapters: Any | None = None,
1490
+ runtimes: dict[str, str] | None = None,
1491
+ keep: bool | str | None = None,
1468
1492
  ) -> RemoteRun:
1469
1493
  """Shared implementation behind ``submit``/``call`` (and legacy aliases)."""
1470
1494
 
1495
+ if adapters is not None:
1496
+ raise NotImplementedError(
1497
+ "run-level adapter overrides are supported only by local Deployment.run in 0.4.0; "
1498
+ "SPLClient daemon runs cannot serialize Python adapter callables yet"
1499
+ )
1500
+
1471
1501
  remote = target_machine is not None or owner is not None or library is not None
1472
- state = self._daemon.run(
1473
- name,
1474
- args=args,
1475
- kwargs=kwargs,
1476
- output=output,
1477
- timeout_seconds=timeout_seconds,
1478
- target_machine=target_machine,
1479
- object_owner_id=owner,
1480
- library=library,
1481
- offline_policy=offline_policy,
1482
- function=function,
1483
- source=source,
1484
- remote=remote or None,
1485
- )
1502
+ run_kwargs: dict[str, Any] = {
1503
+ "args": args,
1504
+ "kwargs": kwargs,
1505
+ "output": output,
1506
+ "timeout_seconds": timeout_seconds,
1507
+ "target_machine": target_machine,
1508
+ "object_owner_id": owner,
1509
+ "library": library,
1510
+ "offline_policy": offline_policy,
1511
+ "function": function,
1512
+ "source": source,
1513
+ "remote": remote or None,
1514
+ }
1515
+ if runtimes is not None:
1516
+ run_kwargs["runtimes"] = runtimes
1517
+ if keep is not None:
1518
+ run_kwargs["keep"] = keep
1519
+ state = self._daemon.run(name, **run_kwargs)
1486
1520
  return RemoteRun(self, state, server_side=remote)
1487
1521
 
1488
1522
  # ``start()`` warned through 0.1.4/0.1.5 and was removed in 0.2.0 — use
@@ -1502,12 +1536,17 @@ class SPLClient:
1502
1536
  offline_policy: OfflinePolicy | None = None,
1503
1537
  function: str | None = None,
1504
1538
  source: RunSource = "auto",
1539
+ adapters: Any | None = None,
1540
+ runtimes: dict[str, str] | None = None,
1541
+ keep: bool | str | None = None,
1505
1542
  ) -> RemoteRun:
1506
1543
  """Canonical async entry point: start a run, return a handle immediately.
1507
1544
 
1508
1545
  The default path is local daemon execution. Passing ``target_machine``,
1509
1546
  ``owner``, or ``library`` intentionally selects central-server remote
1510
- execution through the connected daemon.
1547
+ execution through the connected daemon. ``adapters`` is reserved for
1548
+ run-level adapter overrides and currently raises because daemon runs do
1549
+ not serialize Python adapter callables.
1511
1550
  """
1512
1551
 
1513
1552
  return self._start_run(
@@ -1522,6 +1561,9 @@ class SPLClient:
1522
1561
  offline_policy=offline_policy,
1523
1562
  function=function,
1524
1563
  source=source,
1564
+ adapters=adapters,
1565
+ runtimes=runtimes,
1566
+ keep=keep,
1525
1567
  )
1526
1568
 
1527
1569
  # ``queue()`` warned through 0.1.4/0.1.5 and was removed in 0.2.0 — use
@@ -1542,6 +1584,9 @@ class SPLClient:
1542
1584
  offline_policy: OfflinePolicy | None = None,
1543
1585
  function: str | None = None,
1544
1586
  source: RunSource = "auto",
1587
+ adapters: Any | None = None,
1588
+ runtimes: dict[str, str] | None = None,
1589
+ keep: bool | str | None = None,
1545
1590
  progress: ProgressOption = True,
1546
1591
  ) -> RemoteResult:
1547
1592
  """Run an object, wait for completion, and return result/artifacts.
@@ -1554,7 +1599,9 @@ class SPLClient:
1554
1599
  While waiting, slow phases (a first-run environment build, a queued
1555
1600
  server-side run) print short progress lines to stderr. Pass
1556
1601
  ``progress=False`` to wait silently, or a callable to receive every
1557
- polled run state instead.
1602
+ polled run state instead. ``adapters`` is reserved for run-level
1603
+ adapter overrides and currently raises because daemon runs do not
1604
+ serialize Python adapter callables.
1558
1605
  """
1559
1606
 
1560
1607
  run = self._start_run(
@@ -1569,6 +1616,9 @@ class SPLClient:
1569
1616
  offline_policy=offline_policy,
1570
1617
  function=function,
1571
1618
  source=source,
1619
+ adapters=adapters,
1620
+ runtimes=runtimes,
1621
+ keep=keep,
1572
1622
  )
1573
1623
  return run.collect(
1574
1624
  artifacts_dir=artifacts_dir,
@@ -1628,11 +1678,7 @@ class SPLClient:
1628
1678
  *,
1629
1679
  version: int | None = None,
1630
1680
  ) -> dict[str, Any]:
1631
- ref = (
1632
- self._remote_node_payload(remote, version=version)
1633
- if self._is_node_remote(remote)
1634
- else dict(remote)
1635
- )
1681
+ ref = self._remote_node_payload(remote, version=version) if self._is_node_remote(remote) else dict(remote)
1636
1682
  if version is not None:
1637
1683
  ref["version"] = version
1638
1684
  return self._daemon.resolve_remote_decomposition(ref)
@@ -1735,11 +1781,7 @@ def export_objects_to_yaml(xs: list[Any], *, frame_offset: int = 2) -> str:
1735
1781
 
1736
1782
  top_level_deps = get_top_level_deps(frame_offset, xs)
1737
1783
 
1738
- mapping = {
1739
- root: DSPLSelfImport(name=cast(Any, root).name)
1740
- for (root, _) in top_level_deps
1741
- if hasattr(root, "name")
1742
- }
1784
+ mapping = {root: DSPLSelfImport(name=cast(Any, root).name) for (root, _) in top_level_deps if hasattr(root, "name")}
1743
1785
 
1744
1786
  normalized_deps = {
1745
1787
  root: [mapping.get(dependency, dependency) for dependency in dependencies]
@@ -1762,8 +1804,7 @@ def prepare_export_object(obj: Any, entrypoint: str | None) -> tuple[Any, str]:
1762
1804
  if entrypoint is None:
1763
1805
  if obj.name is None:
1764
1806
  raise ValueError(
1765
- "unnamed pipeline requires entrypoint; "
1766
- "use pipeline.render(name) or publish(..., entrypoint='name')"
1807
+ "unnamed pipeline requires entrypoint; use pipeline.render(name) or publish(..., entrypoint='name')"
1767
1808
  )
1768
1809
  return obj, obj.name
1769
1810
  return replace(obj, name=entrypoint), entrypoint
@@ -7,6 +7,9 @@ from functools import lru_cache
7
7
  from typing import Any
8
8
  from urllib.request import Request, urlopen
9
9
 
10
+ DEFAULT_HTTP_TIMEOUT_SECONDS = 60.0
11
+ DEFAULT_FILE_TRANSFER_TIMEOUT_SECONDS = 300.0
12
+
10
13
 
11
14
  @lru_cache(maxsize=1)
12
15
  def verified_https_context() -> ssl.SSLContext:
@@ -17,7 +20,11 @@ def verified_https_context() -> ssl.SSLContext:
17
20
  return ssl.create_default_context(cafile=certifi.where())
18
21
 
19
22
 
20
- def urlopen_verified(request: Request) -> Any:
23
+ def urlopen_verified(
24
+ request: Request,
25
+ *,
26
+ timeout: float | None = DEFAULT_HTTP_TIMEOUT_SECONDS,
27
+ ) -> Any:
21
28
  """Open a URL request with the bundled certifi CA bundle."""
22
29
 
23
- return urlopen(request, context=verified_https_context())
30
+ return urlopen(request, context=verified_https_context(), timeout=timeout)