splime 0.4.2__tar.gz → 0.4.3__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.
- {splime-0.4.2/src/splime.egg-info → splime-0.4.3}/PKG-INFO +1 -1
- {splime-0.4.2 → splime-0.4.3}/pyproject.toml +1 -1
- {splime-0.4.2 → splime-0.4.3}/src/spl/_client.py +247 -52
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/cli.py +66 -2
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/doctor.py +80 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/heartbeat_service.py +45 -10
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/remote_client.py +9 -2
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/library.py +6 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/object.py +233 -2
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/run.py +4 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/server_connection.py +424 -60
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/sync_event.py +57 -4
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/_helpers.py +28 -14
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/artifacts.py +15 -4
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/diagnostics.py +4 -3
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/libraries.py +17 -10
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/objects.py +32 -12
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/remote.py +5 -5
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/runs.py +15 -3
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/server_connections.py +29 -14
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/runtime_dependencies.py +4 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/server.py +668 -74
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/server_connection.py +73 -15
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/store.py +63 -2
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon_client.py +300 -10
- {splime-0.4.2 → splime-0.4.3/src/splime.egg-info}/PKG-INFO +1 -1
- {splime-0.4.2 → splime-0.4.3}/LICENSE +0 -0
- {splime-0.4.2 → splime-0.4.3}/NOTICE +0 -0
- {splime-0.4.2 → splime-0.4.3}/README.md +0 -0
- {splime-0.4.2 → splime-0.4.3}/setup.cfg +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/_deprecate.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/_http.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/_views.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/client.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/_common.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/adapter_compat.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/common.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/adapter.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/artifact.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/control.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/distribution.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/function.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/local_function.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/misc.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/module.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/node.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/node_function.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/node_remote.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/pipeline.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/entities/scalar.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/fingerprint.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/ir/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/ir/common.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/ir/parse.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/ir/unparse.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/ir/utils.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/manifest.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/node_runtime.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/core/resume.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/__main__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/canonical.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/client.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/docker_environment.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/docker_pool.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/environment.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/environment_base.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/interpreter_visibility.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/metadata.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/repositories/env.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/routes/envs.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/run_progress.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/runtime_backend.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/runtime_config.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/secret_store.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/services/__init__.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/services/sync.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/signature.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/spl_free_generator.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/spl_free_runner.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/storage_base.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/worker.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/daemon/worker_runtime_marker.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/pipeline_widget.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/py.typed +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/spl/server_client.py +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/splime.egg-info/SOURCES.txt +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/splime.egg-info/dependency_links.txt +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/splime.egg-info/entry_points.txt +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/splime.egg-info/requires.txt +0 -0
- {splime-0.4.2 → splime-0.4.3}/src/splime.egg-info/top_level.txt +0 -0
|
@@ -27,6 +27,7 @@ from pathlib import Path
|
|
|
27
27
|
from typing import Any, Literal, cast, overload
|
|
28
28
|
|
|
29
29
|
from spl._views import (
|
|
30
|
+
ActionReceiptView,
|
|
30
31
|
ConnectionStatusView,
|
|
31
32
|
DecompositionView,
|
|
32
33
|
EnvTableView,
|
|
@@ -62,6 +63,11 @@ RunSource = Literal["auto", "local"]
|
|
|
62
63
|
RunIdNamespace = Literal["local", "daemon", "unknown"]
|
|
63
64
|
ProgressOption = bool | RunStateCallback
|
|
64
65
|
_NO_SERVER_CONNECTION_MESSAGE = "active server connection is not found"
|
|
66
|
+
_SERVER_UNREACHABLE_CODE = "central_server_unreachable"
|
|
67
|
+
_SERVER_UNREACHABLE_MESSAGES = (
|
|
68
|
+
"central SPL daemon server is not reachable",
|
|
69
|
+
"central SPL server is not reachable",
|
|
70
|
+
)
|
|
65
71
|
_LIBRARY_DELETE_UNSUPPORTED_MESSAGE = (
|
|
66
72
|
"Deleting central-server libraries is not supported by the SPL server API. "
|
|
67
73
|
"Use the Console archive action to hide a library, or remove individual "
|
|
@@ -84,6 +90,24 @@ def _is_missing_server_connection(exc: Exception) -> bool:
|
|
|
84
90
|
return isinstance(exc, ClientError) and _NO_SERVER_CONNECTION_MESSAGE in message
|
|
85
91
|
|
|
86
92
|
|
|
93
|
+
def _is_server_unreachable(exc: Exception) -> bool:
|
|
94
|
+
"""Return whether the daemon says the central server cannot be reached."""
|
|
95
|
+
|
|
96
|
+
if not isinstance(exc, ClientError):
|
|
97
|
+
return False
|
|
98
|
+
if getattr(exc, "code", None) == _SERVER_UNREACHABLE_CODE:
|
|
99
|
+
return True
|
|
100
|
+
payload = getattr(exc, "payload", None)
|
|
101
|
+
if isinstance(payload, Mapping) and payload.get("code") == _SERVER_UNREACHABLE_CODE:
|
|
102
|
+
return True
|
|
103
|
+
message = str(exc)
|
|
104
|
+
return _SERVER_UNREACHABLE_CODE in message or any(text in message for text in _SERVER_UNREACHABLE_MESSAGES)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_server_unavailable(exc: Exception) -> bool:
|
|
108
|
+
return _is_missing_server_connection(exc) or _is_server_unreachable(exc)
|
|
109
|
+
|
|
110
|
+
|
|
87
111
|
def _is_404_error(exc: Exception) -> bool:
|
|
88
112
|
return isinstance(exc, ClientError) and str(exc).startswith("404:")
|
|
89
113
|
|
|
@@ -126,6 +150,39 @@ def _with_local_cache_warning(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
126
150
|
return {**dict(payload), "warning": _LOCAL_CACHE_ONLY_WARNING}
|
|
127
151
|
|
|
128
152
|
|
|
153
|
+
def _server_connection_mapping(state: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
154
|
+
connection = state.get("connection") or state.get("remote_connection") or {}
|
|
155
|
+
return connection if isinstance(connection, Mapping) else {}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _state_is_server_unreachable(state: Mapping[str, Any]) -> bool:
|
|
159
|
+
if state.get("code") == _SERVER_UNREACHABLE_CODE:
|
|
160
|
+
return True
|
|
161
|
+
return bool(state.get("offline")) and bool(_server_connection_mapping(state))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _state_has_server_connection(state: Mapping[str, Any]) -> bool:
|
|
165
|
+
if _state_is_server_unreachable(state):
|
|
166
|
+
return False
|
|
167
|
+
connected = state.get("connected")
|
|
168
|
+
if connected is not None:
|
|
169
|
+
return bool(connected)
|
|
170
|
+
return _server_connection_mapping(state).get("status") == "connected"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _normalize_server_connection_state(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
174
|
+
normalized = dict(state)
|
|
175
|
+
normalized.setdefault("connected", bool(normalized.get("server_url")))
|
|
176
|
+
if _state_is_server_unreachable(normalized):
|
|
177
|
+
normalized["connected"] = False
|
|
178
|
+
normalized["offline"] = True
|
|
179
|
+
elif not normalized.get("connected"):
|
|
180
|
+
normalized.setdefault("offline", bool(_server_connection_mapping(normalized)))
|
|
181
|
+
else:
|
|
182
|
+
normalized.setdefault("offline", False)
|
|
183
|
+
return normalized
|
|
184
|
+
|
|
185
|
+
|
|
129
186
|
def _progress_callback(progress: ProgressOption) -> RunStateCallback | None:
|
|
130
187
|
"""Turn a ``progress`` option into a wait-loop state callback.
|
|
131
188
|
|
|
@@ -205,6 +262,7 @@ class _BareServerRef:
|
|
|
205
262
|
|
|
206
263
|
|
|
207
264
|
_CATALOG_HEADERS = ("name", "kind", "version", "library", "inputs")
|
|
265
|
+
_CATALOG_HEADERS_WITH_OWNER = ("name", "kind", "version", "owner", "library", "inputs")
|
|
208
266
|
|
|
209
267
|
|
|
210
268
|
def _catalog_rows(
|
|
@@ -237,6 +295,7 @@ def _catalog_rows(
|
|
|
237
295
|
"name": str(record.get("display_name") or record.get("name") or ""),
|
|
238
296
|
"kind": str(record.get("kind") or ""),
|
|
239
297
|
"version": str(version_value or ""),
|
|
298
|
+
"owner": str(record.get("owner_id") or ""),
|
|
240
299
|
"library": str(library_name or ""),
|
|
241
300
|
"inputs": str(len(record.get("inputs") or [])),
|
|
242
301
|
}
|
|
@@ -244,20 +303,26 @@ def _catalog_rows(
|
|
|
244
303
|
return rows
|
|
245
304
|
|
|
246
305
|
|
|
306
|
+
def _catalog_headers(rows: list[dict[str, str]]) -> tuple[str, ...]:
|
|
307
|
+
owners = {row.get("owner") or "" for row in rows}
|
|
308
|
+
owners.discard("")
|
|
309
|
+
return _CATALOG_HEADERS_WITH_OWNER if len(owners) > 1 else _CATALOG_HEADERS
|
|
310
|
+
|
|
311
|
+
|
|
247
312
|
def _rows_to_text(rows: list[dict[str, str]], title: str) -> str:
|
|
248
313
|
if not rows:
|
|
249
314
|
return f"{title}: (empty)"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
315
|
+
headers = _catalog_headers(rows)
|
|
316
|
+
widths = {header: max(len(header), *(len(row[header]) for row in rows)) for header in headers}
|
|
317
|
+
head = " ".join(header.ljust(widths[header]) for header in headers)
|
|
318
|
+
body = "\n".join(" ".join(row[header].ljust(widths[header]) for header in headers) for row in rows)
|
|
253
319
|
return f"{title} ({len(rows)}):\n{head}\n{body}"
|
|
254
320
|
|
|
255
321
|
|
|
256
322
|
def _rows_to_html(rows: list[dict[str, str]], title: str) -> str:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
)
|
|
323
|
+
headers = _catalog_headers(rows)
|
|
324
|
+
head = "".join(f"<th style='text-align:left'>{escape(header)}</th>" for header in headers)
|
|
325
|
+
body = "".join("<tr>" + "".join(f"<td>{escape(row[header])}</td>" for header in headers) + "</tr>" for row in rows)
|
|
261
326
|
return (
|
|
262
327
|
f"<div><b>{escape(title)}</b> ({len(rows)})"
|
|
263
328
|
f"<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>"
|
|
@@ -579,7 +644,7 @@ class RemoteRun:
|
|
|
579
644
|
poll_interval: float = 0.25,
|
|
580
645
|
timeout_seconds: float | None = None,
|
|
581
646
|
progress: ProgressOption = True,
|
|
582
|
-
) ->
|
|
647
|
+
) -> RunRecordView:
|
|
583
648
|
"""Wait until the run succeeds or fails, then return final state.
|
|
584
649
|
|
|
585
650
|
``progress`` controls feedback during slow phases (first-run
|
|
@@ -965,9 +1030,10 @@ class SPLClient:
|
|
|
965
1030
|
except Exception as exc:
|
|
966
1031
|
if _is_missing_server_connection(exc):
|
|
967
1032
|
return ConnectionStatusView({"connected": False, "offline": False, "connection": None})
|
|
1033
|
+
if _is_server_unreachable(exc):
|
|
1034
|
+
return ConnectionStatusView({"connected": False, "offline": True, "connection": None})
|
|
968
1035
|
raise
|
|
969
|
-
|
|
970
|
-
return ConnectionStatusView(state)
|
|
1036
|
+
return ConnectionStatusView(_normalize_server_connection_state(state))
|
|
971
1037
|
|
|
972
1038
|
def machines(self) -> dict[str, Any]:
|
|
973
1039
|
"""Return the user's machines, or an empty listing when not connected.
|
|
@@ -983,13 +1049,13 @@ class SPLClient:
|
|
|
983
1049
|
try:
|
|
984
1050
|
self._daemon.server_connection()
|
|
985
1051
|
except Exception as exc:
|
|
986
|
-
if not
|
|
1052
|
+
if not _is_server_unavailable(exc):
|
|
987
1053
|
raise
|
|
988
1054
|
return MachineListView({"current_machine_id": None, "machines": []})
|
|
989
1055
|
try:
|
|
990
1056
|
machines = self._daemon.server_machines()
|
|
991
1057
|
except Exception as exc:
|
|
992
|
-
if not
|
|
1058
|
+
if not _is_server_unavailable(exc):
|
|
993
1059
|
raise
|
|
994
1060
|
return MachineListView({"current_machine_id": None, "machines": []})
|
|
995
1061
|
return MachineListView(machines)
|
|
@@ -999,7 +1065,13 @@ class SPLClient:
|
|
|
999
1065
|
|
|
1000
1066
|
if not self._has_server_connection():
|
|
1001
1067
|
return LibraryListView([])
|
|
1002
|
-
|
|
1068
|
+
try:
|
|
1069
|
+
libraries = self._daemon.server_libraries(include_accessible=include_accessible)
|
|
1070
|
+
except Exception as exc:
|
|
1071
|
+
if not _is_server_unavailable(exc):
|
|
1072
|
+
raise
|
|
1073
|
+
libraries = []
|
|
1074
|
+
return LibraryListView(libraries)
|
|
1003
1075
|
|
|
1004
1076
|
# The flat library aliases (create_library, get_library, update_library,
|
|
1005
1077
|
# delete_library, grant_library, revoke_library_grant, add_reference,
|
|
@@ -1196,6 +1268,7 @@ class SPLClient:
|
|
|
1196
1268
|
accessible server catalog when connected and local-only when offline.
|
|
1197
1269
|
"""
|
|
1198
1270
|
|
|
1271
|
+
requested_scope = scope
|
|
1199
1272
|
if scope == "auto":
|
|
1200
1273
|
scope = "server" if owner is not None or library is not None or self._has_server_connection() else "local"
|
|
1201
1274
|
if scope == "local":
|
|
@@ -1205,25 +1278,34 @@ class SPLClient:
|
|
|
1205
1278
|
if scope == "server":
|
|
1206
1279
|
if owner is None and library is None and not self._has_server_connection():
|
|
1207
1280
|
return ObjectList([], title=object_scope_title("server"))
|
|
1208
|
-
|
|
1209
|
-
self._daemon.server_objects(
|
|
1281
|
+
try:
|
|
1282
|
+
server_objects = self._daemon.server_objects(
|
|
1210
1283
|
owner_id=owner,
|
|
1211
1284
|
library=library,
|
|
1212
1285
|
compact=compact,
|
|
1213
|
-
)
|
|
1214
|
-
|
|
1215
|
-
|
|
1286
|
+
)
|
|
1287
|
+
except Exception as exc:
|
|
1288
|
+
if not _is_server_unavailable(exc):
|
|
1289
|
+
raise
|
|
1290
|
+
if requested_scope == "auto" and owner is None and library is None:
|
|
1291
|
+
return _wrap_objects(self._daemon.list_objects(compact=compact), title=object_scope_title("local"))
|
|
1292
|
+
server_objects = []
|
|
1293
|
+
return _wrap_objects(server_objects, title=object_scope_title("server"))
|
|
1216
1294
|
if scope == "all":
|
|
1217
1295
|
local_objects = self._daemon.list_objects(compact=compact)
|
|
1218
|
-
|
|
1219
|
-
[]
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1296
|
+
if owner is None and library is None and not self._has_server_connection():
|
|
1297
|
+
server_objects = []
|
|
1298
|
+
else:
|
|
1299
|
+
try:
|
|
1300
|
+
server_objects = self._daemon.server_objects(
|
|
1301
|
+
owner_id=owner,
|
|
1302
|
+
library=library,
|
|
1303
|
+
compact=compact,
|
|
1304
|
+
)
|
|
1305
|
+
except Exception as exc:
|
|
1306
|
+
if not _is_server_unavailable(exc):
|
|
1307
|
+
raise
|
|
1308
|
+
server_objects = []
|
|
1227
1309
|
return ObjectCatalog(
|
|
1228
1310
|
{
|
|
1229
1311
|
"local": local_objects,
|
|
@@ -1292,19 +1374,82 @@ class SPLClient:
|
|
|
1292
1374
|
"stale mirrors pruned",
|
|
1293
1375
|
)
|
|
1294
1376
|
|
|
1377
|
+
def pull(
|
|
1378
|
+
self,
|
|
1379
|
+
name: str,
|
|
1380
|
+
*,
|
|
1381
|
+
owner: str | None = None,
|
|
1382
|
+
library: str | None = None,
|
|
1383
|
+
version: int | None = None,
|
|
1384
|
+
all_versions: bool = False,
|
|
1385
|
+
) -> ActionReceiptView:
|
|
1386
|
+
"""Mirror one accessible server object into the local daemon cache.
|
|
1387
|
+
|
|
1388
|
+
A pulled server-origin mirror can be inspected and called while the
|
|
1389
|
+
daemon is offline. The method requires an active server connection for
|
|
1390
|
+
the download itself; repeat pulls are idempotent and report unchanged
|
|
1391
|
+
versions under ``skipped``.
|
|
1392
|
+
"""
|
|
1393
|
+
|
|
1394
|
+
if not self._has_server_connection():
|
|
1395
|
+
raise ClientError(self._pull_requires_server_connection_message())
|
|
1396
|
+
resolved_name = name
|
|
1397
|
+
owner_id = owner
|
|
1398
|
+
resolved_library = library
|
|
1399
|
+
if owner is None and library is None:
|
|
1400
|
+
ref = self._resolve_bare_server_ref(name, example_method="pull")
|
|
1401
|
+
resolved_name = ref.name
|
|
1402
|
+
owner_id = ref.owner_id
|
|
1403
|
+
resolved_library = ref.library
|
|
1404
|
+
return wrap_action(
|
|
1405
|
+
self._daemon.pull_server_object(
|
|
1406
|
+
resolved_name,
|
|
1407
|
+
owner_id=owner_id,
|
|
1408
|
+
library=resolved_library,
|
|
1409
|
+
version=version,
|
|
1410
|
+
all_versions=all_versions,
|
|
1411
|
+
),
|
|
1412
|
+
"object pulled",
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
def pull_all(
|
|
1416
|
+
self,
|
|
1417
|
+
*,
|
|
1418
|
+
owner: str | None = None,
|
|
1419
|
+
library: str | None = None,
|
|
1420
|
+
all_versions: bool = False,
|
|
1421
|
+
dry_run: bool = False,
|
|
1422
|
+
) -> ActionReceiptView:
|
|
1423
|
+
"""Mirror the visible server catalog into the local daemon cache.
|
|
1424
|
+
|
|
1425
|
+
The visible catalog can be large. Start with ``dry_run=True`` and
|
|
1426
|
+
narrow the batch with ``owner=`` or ``library=`` before downloading a
|
|
1427
|
+
whole catalog. ``dry_run`` returns the same receipt shape without
|
|
1428
|
+
writing local mirror rows or YAML files.
|
|
1429
|
+
"""
|
|
1430
|
+
|
|
1431
|
+
if not self._has_server_connection():
|
|
1432
|
+
raise ClientError(self._pull_all_requires_server_connection_message())
|
|
1433
|
+
return wrap_action(
|
|
1434
|
+
self._daemon.pull_all_server_objects(
|
|
1435
|
+
owner_id=owner,
|
|
1436
|
+
library=library,
|
|
1437
|
+
all_versions=all_versions,
|
|
1438
|
+
dry_run=dry_run,
|
|
1439
|
+
),
|
|
1440
|
+
"server catalog pull plan" if dry_run else "server catalog pulled",
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1295
1443
|
def _has_server_connection(self) -> bool:
|
|
1296
1444
|
if self.server_connection is not None:
|
|
1297
|
-
return
|
|
1445
|
+
return _state_has_server_connection(self.server_connection)
|
|
1298
1446
|
try:
|
|
1299
1447
|
state = self._daemon.server_connection()
|
|
1300
1448
|
except Exception as exc:
|
|
1301
|
-
if
|
|
1449
|
+
if _is_server_unavailable(exc):
|
|
1302
1450
|
return False
|
|
1303
1451
|
raise
|
|
1304
|
-
|
|
1305
|
-
return True
|
|
1306
|
-
connection = state.get("connection") or state.get("remote_connection") or {}
|
|
1307
|
-
return connection.get("status") == "connected"
|
|
1452
|
+
return _state_has_server_connection(state)
|
|
1308
1453
|
|
|
1309
1454
|
def _server_connection_label(self) -> str:
|
|
1310
1455
|
state = self.server_connection
|
|
@@ -1323,7 +1468,39 @@ class SPLClient:
|
|
|
1323
1468
|
return str(value)
|
|
1324
1469
|
return "the active server connection"
|
|
1325
1470
|
|
|
1326
|
-
|
|
1471
|
+
@staticmethod
|
|
1472
|
+
def _offline_bare_server_ref_message(name: str) -> str:
|
|
1473
|
+
return (
|
|
1474
|
+
"404: {!r} is not registered locally and the daemon has no server connection; "
|
|
1475
|
+
"reconnect with client.connect_server(...) to search the server catalog, or "
|
|
1476
|
+
"client.pull(...) the object next time you are online."
|
|
1477
|
+
).format(name)
|
|
1478
|
+
|
|
1479
|
+
@staticmethod
|
|
1480
|
+
def _unreachable_bare_server_ref_message(name: str) -> str:
|
|
1481
|
+
return (
|
|
1482
|
+
"404: {!r} is not registered locally; server unreachable; "
|
|
1483
|
+
"mirror it with client.pull(...) when online, or reconnect with "
|
|
1484
|
+
"client.connect_server(...) to search the server catalog."
|
|
1485
|
+
).format(name)
|
|
1486
|
+
|
|
1487
|
+
@staticmethod
|
|
1488
|
+
def _pull_requires_server_connection_message() -> str:
|
|
1489
|
+
return (
|
|
1490
|
+
"404: pull requires a server connection; the daemon has no server connection. "
|
|
1491
|
+
"Reconnect with client.connect_server(...) to search the server catalog, "
|
|
1492
|
+
"then retry client.pull(...)."
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
@staticmethod
|
|
1496
|
+
def _pull_all_requires_server_connection_message() -> str:
|
|
1497
|
+
return (
|
|
1498
|
+
"404: pull_all requires a server connection; the daemon has no server connection. "
|
|
1499
|
+
"Reconnect with client.connect_server(...) to search the server catalog, "
|
|
1500
|
+
"then retry client.pull_all(...)."
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
def _resolve_bare_server_ref(self, name: str, *, example_method: str = "signature") -> _BareServerRef:
|
|
1327
1504
|
"""Resolve a bare object name through the visible server catalog after a local 404.
|
|
1328
1505
|
|
|
1329
1506
|
This is intentionally catalog-driven: no default library is assumed.
|
|
@@ -1331,12 +1508,30 @@ class SPLClient:
|
|
|
1331
1508
|
``owner=``/``library=`` explicitly.
|
|
1332
1509
|
"""
|
|
1333
1510
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1511
|
+
try:
|
|
1512
|
+
connection_state = (
|
|
1513
|
+
self.server_connection if self.server_connection is not None else self._daemon.server_connection()
|
|
1514
|
+
)
|
|
1515
|
+
except Exception as exc:
|
|
1516
|
+
if _is_server_unreachable(exc):
|
|
1517
|
+
raise ClientError(self._unreachable_bare_server_ref_message(name)) from exc
|
|
1518
|
+
if _is_missing_server_connection(exc):
|
|
1519
|
+
raise ClientError(self._offline_bare_server_ref_message(name)) from exc
|
|
1520
|
+
raise
|
|
1521
|
+
if not _state_has_server_connection(connection_state):
|
|
1522
|
+
if _state_is_server_unreachable(connection_state):
|
|
1523
|
+
raise ClientError(self._unreachable_bare_server_ref_message(name))
|
|
1524
|
+
raise ClientError(self._offline_bare_server_ref_message(name))
|
|
1525
|
+
try:
|
|
1526
|
+
server_records = self._daemon.server_objects(compact=True)
|
|
1527
|
+
except Exception as exc:
|
|
1528
|
+
if _is_server_unreachable(exc):
|
|
1529
|
+
raise ClientError(self._unreachable_bare_server_ref_message(name)) from exc
|
|
1530
|
+
if _is_missing_server_connection(exc):
|
|
1531
|
+
raise ClientError(self._offline_bare_server_ref_message(name)) from exc
|
|
1532
|
+
raise
|
|
1336
1533
|
candidates = [
|
|
1337
|
-
record
|
|
1338
|
-
for record in self._daemon.server_objects(compact=True)
|
|
1339
|
-
if isinstance(record, Mapping) and name in _server_record_names(record)
|
|
1534
|
+
record for record in server_records if isinstance(record, Mapping) and name in _server_record_names(record)
|
|
1340
1535
|
]
|
|
1341
1536
|
if len(candidates) == 1:
|
|
1342
1537
|
record = candidates[0]
|
|
@@ -1351,7 +1546,7 @@ class SPLClient:
|
|
|
1351
1546
|
raise ClientError(
|
|
1352
1547
|
"{!r} is not registered locally; found on the server in: {}. "
|
|
1353
1548
|
"Pass library=... and owner=... to disambiguate, for example "
|
|
1354
|
-
"client.
|
|
1549
|
+
"client.{}({!r}, library='...').".format(name, choices, example_method, name)
|
|
1355
1550
|
)
|
|
1356
1551
|
raise ClientError(
|
|
1357
1552
|
"{!r} is not registered locally; no accessible server object named {!r} (connected as {}). "
|
|
@@ -1384,8 +1579,6 @@ class SPLClient:
|
|
|
1384
1579
|
if owner is not None or library is not None or not _local_object_missing(exc):
|
|
1385
1580
|
raise
|
|
1386
1581
|
ref = self._resolve_bare_server_ref(name)
|
|
1387
|
-
if ref is None:
|
|
1388
|
-
raise
|
|
1389
1582
|
signature = dict(
|
|
1390
1583
|
self._daemon.signature(
|
|
1391
1584
|
ref.name,
|
|
@@ -1537,8 +1730,6 @@ class SPLClient:
|
|
|
1537
1730
|
if not _local_object_missing(exc):
|
|
1538
1731
|
raise
|
|
1539
1732
|
ref = self._resolve_bare_server_ref(str(name))
|
|
1540
|
-
if ref is None:
|
|
1541
|
-
raise
|
|
1542
1733
|
response = self._remote_decomposition_response(
|
|
1543
1734
|
{
|
|
1544
1735
|
"name": ref.name,
|
|
@@ -1626,8 +1817,6 @@ class SPLClient:
|
|
|
1626
1817
|
if not _local_object_missing(exc):
|
|
1627
1818
|
raise
|
|
1628
1819
|
ref = self._resolve_bare_server_ref(pipeline)
|
|
1629
|
-
if ref is None:
|
|
1630
|
-
raise
|
|
1631
1820
|
response = self._remote_decomposition_response(
|
|
1632
1821
|
{
|
|
1633
1822
|
"name": ref.name,
|
|
@@ -1717,12 +1906,18 @@ class SPLClient:
|
|
|
1717
1906
|
)
|
|
1718
1907
|
freshness_line = None
|
|
1719
1908
|
if function is None and "resolved_from_server" not in signature and self._has_server_connection():
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1909
|
+
try:
|
|
1910
|
+
server_records = self._daemon.server_objects(compact=True)
|
|
1911
|
+
except Exception as exc:
|
|
1912
|
+
if not _is_server_unavailable(exc):
|
|
1913
|
+
raise
|
|
1914
|
+
else:
|
|
1915
|
+
freshness_line = _server_freshness_line(
|
|
1916
|
+
signature,
|
|
1917
|
+
server_records,
|
|
1918
|
+
owner=owner,
|
|
1919
|
+
library=library,
|
|
1920
|
+
)
|
|
1726
1921
|
display_name = signature.get("display_name") or signature["name"]
|
|
1727
1922
|
lines = [(f"{display_name} v{signature['version']} ({signature['kind']})")]
|
|
1728
1923
|
server_resolution = _server_resolution_label(signature.get("resolved_from_server"))
|
|
@@ -205,6 +205,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
205
205
|
"server-connections",
|
|
206
206
|
help="list stored central-server connection attempts",
|
|
207
207
|
)
|
|
208
|
+
subparsers.add_parser(
|
|
209
|
+
"connections-list",
|
|
210
|
+
help="list stored central-server connection attempts",
|
|
211
|
+
)
|
|
212
|
+
connections_prune = subparsers.add_parser(
|
|
213
|
+
"connections-prune",
|
|
214
|
+
help="prune stale stored central-server connection attempts",
|
|
215
|
+
)
|
|
216
|
+
connections_prune.add_argument(
|
|
217
|
+
"--older-than-days",
|
|
218
|
+
type=int,
|
|
219
|
+
default=30,
|
|
220
|
+
help="also prune stale non-current rows older than this many days",
|
|
221
|
+
)
|
|
222
|
+
connections_prune.add_argument(
|
|
223
|
+
"--dry-run",
|
|
224
|
+
action="store_true",
|
|
225
|
+
help="show stale rows without deleting them",
|
|
226
|
+
)
|
|
208
227
|
subparsers.add_parser(
|
|
209
228
|
"server-machines",
|
|
210
229
|
help="list machines visible to the connected user",
|
|
@@ -369,6 +388,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
369
388
|
)
|
|
370
389
|
object_versions.add_argument("name_or_id")
|
|
371
390
|
|
|
391
|
+
pull = subparsers.add_parser("pull", help="mirror server objects into the local daemon cache")
|
|
392
|
+
pull.add_argument("name", nargs="?")
|
|
393
|
+
pull.add_argument("--all", dest="all_objects", action="store_true")
|
|
394
|
+
pull.add_argument("--owner", default=None)
|
|
395
|
+
pull.add_argument("--library", default=None)
|
|
396
|
+
pull.add_argument("--version", type=int, default=None)
|
|
397
|
+
pull.add_argument("--all-versions", action="store_true")
|
|
398
|
+
pull.add_argument("--dry-run", action="store_true")
|
|
399
|
+
|
|
372
400
|
run = subparsers.add_parser("run", help="start an object run")
|
|
373
401
|
run.add_argument("object")
|
|
374
402
|
run.add_argument("--args", default="[]", help="JSON list of positional args")
|
|
@@ -467,7 +495,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
467
495
|
def main(argv: list[str] | None = None) -> int:
|
|
468
496
|
"""Execute the CLI command."""
|
|
469
497
|
|
|
470
|
-
|
|
498
|
+
parser = build_parser()
|
|
499
|
+
args = parser.parse_args(argv)
|
|
471
500
|
|
|
472
501
|
if args.command == "serve":
|
|
473
502
|
try:
|
|
@@ -523,8 +552,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
523
552
|
print_json(client.disconnect_server())
|
|
524
553
|
elif args.command == "server-connection":
|
|
525
554
|
print_json(client.server_connection())
|
|
526
|
-
elif args.command
|
|
555
|
+
elif args.command in {"server-connections", "connections-list"}:
|
|
527
556
|
print_json(client.server_connections())
|
|
557
|
+
elif args.command == "connections-prune":
|
|
558
|
+
print_json(
|
|
559
|
+
client.prune_server_connections(
|
|
560
|
+
older_than_days=args.older_than_days,
|
|
561
|
+
dry_run=args.dry_run,
|
|
562
|
+
)
|
|
563
|
+
)
|
|
528
564
|
elif args.command == "server-machines":
|
|
529
565
|
print_json(client.server_machines())
|
|
530
566
|
elif args.command == "env-add":
|
|
@@ -601,6 +637,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
601
637
|
print_json(client.outputs(args.name_or_id, version=args.version))
|
|
602
638
|
elif args.command == "object-versions":
|
|
603
639
|
print_json(client.object_versions(args.name_or_id))
|
|
640
|
+
elif args.command == "pull":
|
|
641
|
+
if args.all_objects:
|
|
642
|
+
if args.name is not None:
|
|
643
|
+
parser.error("pull --all does not accept a name")
|
|
644
|
+
if args.version is not None:
|
|
645
|
+
parser.error("pull --all does not accept --version")
|
|
646
|
+
print_json(
|
|
647
|
+
client.pull_all_server_objects(
|
|
648
|
+
owner_id=args.owner,
|
|
649
|
+
library=args.library,
|
|
650
|
+
all_versions=args.all_versions,
|
|
651
|
+
dry_run=args.dry_run,
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
if args.name is None:
|
|
656
|
+
parser.error("pull requires a name unless --all is set")
|
|
657
|
+
if args.dry_run:
|
|
658
|
+
parser.error("pull --dry-run requires --all")
|
|
659
|
+
print_json(
|
|
660
|
+
client.pull_server_object(
|
|
661
|
+
args.name,
|
|
662
|
+
owner_id=args.owner,
|
|
663
|
+
library=args.library,
|
|
664
|
+
version=args.version,
|
|
665
|
+
all_versions=args.all_versions,
|
|
666
|
+
)
|
|
667
|
+
)
|
|
604
668
|
elif args.command == "run":
|
|
605
669
|
positional_args = parse_json_arg(args.args, list)
|
|
606
670
|
keyword_args = parse_json_arg(args.kwargs, dict)
|