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/server_client.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""Direct client for the central SPL daemon server.
|
|
2
|
+
|
|
3
|
+
This client is intentionally separate from ``SPLClient``. ``SPLClient`` talks
|
|
4
|
+
to the local daemon first; ``SPLServerClient`` talks to the central server
|
|
5
|
+
directly with one bearer token, which is useful for external execution tokens
|
|
6
|
+
and small service integrations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
from urllib.error import HTTPError, URLError
|
|
17
|
+
from urllib.parse import quote, urlencode
|
|
18
|
+
from urllib.request import Request, urlopen
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_SERVER_URL = "https://splime.io/api"
|
|
22
|
+
TERMINAL_REMOTE_RUN_STATUSES = {"succeeded", "failed", "cancelled", "stale"}
|
|
23
|
+
|
|
24
|
+
OfflinePolicy = Literal["queue", "wait", "fail_fast"]
|
|
25
|
+
RemoteRunScope = Literal["owned", "target", "object", "all"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _url_part(value: str) -> str:
|
|
29
|
+
return quote(value, safe="")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ServerClientError(RuntimeError):
|
|
33
|
+
"""Raised when the central server returns an error response."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, status_code: int, message: str):
|
|
36
|
+
self.status_code = status_code
|
|
37
|
+
self.message = message
|
|
38
|
+
super().__init__(f"{status_code}: {message}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ServerCallResult:
|
|
43
|
+
"""Completed server run plus optional downloaded artifacts."""
|
|
44
|
+
|
|
45
|
+
run: dict[str, Any]
|
|
46
|
+
detail: dict[str, Any]
|
|
47
|
+
downloaded_artifacts: dict[str, Path] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def value(self) -> Any:
|
|
51
|
+
result = self.detail.get("result")
|
|
52
|
+
if isinstance(result, dict) and "value" in result:
|
|
53
|
+
return result["value"]
|
|
54
|
+
if result is not None:
|
|
55
|
+
return result
|
|
56
|
+
raw = self.run.get("result")
|
|
57
|
+
if isinstance(raw, dict) and "value" in raw:
|
|
58
|
+
return raw["value"]
|
|
59
|
+
return raw
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def artifacts(self) -> list[dict[str, Any]]:
|
|
63
|
+
return list(self.detail.get("artifacts") or [])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ServerRemoteRun:
|
|
67
|
+
"""Handle for a central-server remote run."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, client: "SPLServerClient", state: dict[str, Any]):
|
|
70
|
+
self._client = client
|
|
71
|
+
self.state = state
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def id(self) -> str:
|
|
75
|
+
return self.state["id"]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def status(self) -> str:
|
|
79
|
+
return self.state["status"]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def mode(self) -> str:
|
|
83
|
+
return "server"
|
|
84
|
+
|
|
85
|
+
def refresh(self) -> dict[str, Any]:
|
|
86
|
+
self.state = self._client.get_run(self.id)
|
|
87
|
+
return self.state
|
|
88
|
+
|
|
89
|
+
def wait(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
poll_interval: float = 0.5,
|
|
93
|
+
timeout_seconds: float | None = None,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
started = time.monotonic()
|
|
96
|
+
while True:
|
|
97
|
+
state = self.refresh()
|
|
98
|
+
if state["status"] in TERMINAL_REMOTE_RUN_STATUSES:
|
|
99
|
+
return state
|
|
100
|
+
if timeout_seconds is not None and time.monotonic() - started >= timeout_seconds:
|
|
101
|
+
raise TimeoutError(f"remote run {self.id!r} did not finish in time")
|
|
102
|
+
time.sleep(max(0.0, poll_interval))
|
|
103
|
+
|
|
104
|
+
def detail(self) -> dict[str, Any]:
|
|
105
|
+
return self._client.get_run_detail(self.id)
|
|
106
|
+
|
|
107
|
+
def events(self) -> list[dict[str, Any]]:
|
|
108
|
+
return self._client.list_events(self.id)
|
|
109
|
+
|
|
110
|
+
def artifact_names(self) -> list[str]:
|
|
111
|
+
return [item["name"] for item in self._client.list_artifacts(self.id)]
|
|
112
|
+
|
|
113
|
+
def artifact_bytes(self, name: str) -> bytes:
|
|
114
|
+
return self._client.artifact_bytes(self.id, name)
|
|
115
|
+
|
|
116
|
+
def download_artifact(self, name: str, target: str | Path) -> Path:
|
|
117
|
+
return self._client.download_artifact(self.id, name, target)
|
|
118
|
+
|
|
119
|
+
def download_artifacts(self, target_dir: str | Path) -> dict[str, Path]:
|
|
120
|
+
target_path = Path(target_dir)
|
|
121
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
return {
|
|
123
|
+
name: self._client.download_artifact(self.id, name, target_path)
|
|
124
|
+
for name in self.artifact_names()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def cancel(self) -> dict[str, Any]:
|
|
128
|
+
self.state = self._client.cancel_run(self.id)
|
|
129
|
+
return self.state
|
|
130
|
+
|
|
131
|
+
def retry(self) -> "ServerRemoteRun":
|
|
132
|
+
return self._client.retry_run(self.id)
|
|
133
|
+
|
|
134
|
+
def collect(
|
|
135
|
+
self,
|
|
136
|
+
*,
|
|
137
|
+
artifacts_dir: str | Path | None = None,
|
|
138
|
+
poll_interval: float = 0.5,
|
|
139
|
+
timeout_seconds: float | None = None,
|
|
140
|
+
) -> ServerCallResult:
|
|
141
|
+
final_state = self.wait(
|
|
142
|
+
poll_interval=poll_interval,
|
|
143
|
+
timeout_seconds=timeout_seconds,
|
|
144
|
+
)
|
|
145
|
+
if final_state["status"] != "succeeded":
|
|
146
|
+
error = final_state.get("error") or "remote run returned no error message"
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
f"server run {self.id!r} ended as "
|
|
149
|
+
f"{final_state.get('status')!r}: {error}"
|
|
150
|
+
)
|
|
151
|
+
detail = self.detail()
|
|
152
|
+
downloaded = (
|
|
153
|
+
self.download_artifacts(artifacts_dir)
|
|
154
|
+
if artifacts_dir is not None
|
|
155
|
+
else {}
|
|
156
|
+
)
|
|
157
|
+
return ServerCallResult(
|
|
158
|
+
run=final_state,
|
|
159
|
+
detail=detail,
|
|
160
|
+
downloaded_artifacts=downloaded,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SPLServerClient:
|
|
165
|
+
"""Small stdlib HTTP client for the central SPL daemon server."""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
token: str,
|
|
170
|
+
*,
|
|
171
|
+
base_url: str = DEFAULT_SERVER_URL,
|
|
172
|
+
):
|
|
173
|
+
if not token:
|
|
174
|
+
raise ValueError("token is required")
|
|
175
|
+
self.token = token
|
|
176
|
+
self.base_url = base_url.rstrip("/")
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def external_token(
|
|
180
|
+
cls,
|
|
181
|
+
token: str,
|
|
182
|
+
*,
|
|
183
|
+
base_url: str = DEFAULT_SERVER_URL,
|
|
184
|
+
) -> "SPLExternalTokenClient":
|
|
185
|
+
"""Return a restricted facade for ``library_execution_token`` use."""
|
|
186
|
+
|
|
187
|
+
return SPLExternalTokenClient(token, base_url=base_url)
|
|
188
|
+
|
|
189
|
+
def _headers(self) -> dict[str, str]:
|
|
190
|
+
return {
|
|
191
|
+
"Accept": "application/json",
|
|
192
|
+
"Authorization": f"Bearer {self.token}",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def _json_request(
|
|
196
|
+
self,
|
|
197
|
+
method: str,
|
|
198
|
+
path: str,
|
|
199
|
+
payload: dict[str, Any] | None = None,
|
|
200
|
+
) -> Any:
|
|
201
|
+
body = None
|
|
202
|
+
headers = self._headers()
|
|
203
|
+
if payload is not None:
|
|
204
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
205
|
+
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
206
|
+
request = Request(
|
|
207
|
+
f"{self.base_url}{path}",
|
|
208
|
+
data=body,
|
|
209
|
+
headers=headers,
|
|
210
|
+
method=method,
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
with urlopen(request) as response: # noqa: S310 - configured server URL.
|
|
214
|
+
raw = response.read().decode("utf-8")
|
|
215
|
+
except HTTPError as exc:
|
|
216
|
+
raw = exc.read().decode("utf-8")
|
|
217
|
+
try:
|
|
218
|
+
message = json.loads(raw).get("error", raw)
|
|
219
|
+
except json.JSONDecodeError:
|
|
220
|
+
message = raw
|
|
221
|
+
raise ServerClientError(
|
|
222
|
+
exc.code,
|
|
223
|
+
f"central SPL server returned {exc.code} at {self.base_url}{path}: {message}",
|
|
224
|
+
) from exc
|
|
225
|
+
except URLError as exc:
|
|
226
|
+
raise ServerClientError(
|
|
227
|
+
502,
|
|
228
|
+
f"central SPL server is not reachable at {self.base_url}: {exc.reason}",
|
|
229
|
+
) from exc
|
|
230
|
+
if not raw:
|
|
231
|
+
return None
|
|
232
|
+
return json.loads(raw)
|
|
233
|
+
|
|
234
|
+
def _bytes_request(self, path: str) -> bytes:
|
|
235
|
+
request = Request(f"{self.base_url}{path}", headers=self._headers())
|
|
236
|
+
try:
|
|
237
|
+
with urlopen(request) as response: # noqa: S310 - configured server URL.
|
|
238
|
+
return response.read()
|
|
239
|
+
except HTTPError as exc:
|
|
240
|
+
raw = exc.read().decode("utf-8")
|
|
241
|
+
try:
|
|
242
|
+
message = json.loads(raw).get("error", raw)
|
|
243
|
+
except json.JSONDecodeError:
|
|
244
|
+
message = raw
|
|
245
|
+
raise ServerClientError(
|
|
246
|
+
exc.code,
|
|
247
|
+
f"central SPL server returned {exc.code} at {self.base_url}{path}: {message}",
|
|
248
|
+
) from exc
|
|
249
|
+
except URLError as exc:
|
|
250
|
+
raise ServerClientError(
|
|
251
|
+
502,
|
|
252
|
+
f"central SPL server is not reachable at {self.base_url}: {exc.reason}",
|
|
253
|
+
) from exc
|
|
254
|
+
|
|
255
|
+
def objects(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
owner: str | None = None,
|
|
259
|
+
library: str | None = None,
|
|
260
|
+
compact: bool = False,
|
|
261
|
+
) -> list[dict[str, Any]]:
|
|
262
|
+
path = (
|
|
263
|
+
f"/owners/{_url_part(owner)}/libraries/{_url_part(library or 'default')}/objects"
|
|
264
|
+
if owner
|
|
265
|
+
else "/objects"
|
|
266
|
+
)
|
|
267
|
+
query = {}
|
|
268
|
+
if library and owner is None:
|
|
269
|
+
query["library"] = library
|
|
270
|
+
if compact:
|
|
271
|
+
query["view"] = "summary"
|
|
272
|
+
return self._json_request("GET", self._with_query(path, query))
|
|
273
|
+
|
|
274
|
+
def get_object(
|
|
275
|
+
self,
|
|
276
|
+
name_or_id: str,
|
|
277
|
+
*,
|
|
278
|
+
owner: str | None = None,
|
|
279
|
+
library: str | None = None,
|
|
280
|
+
version: int | None = None,
|
|
281
|
+
include_yaml: bool = False,
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
query: dict[str, Any] = {}
|
|
284
|
+
if version is not None:
|
|
285
|
+
query["version"] = int(version)
|
|
286
|
+
if include_yaml:
|
|
287
|
+
query["include_yaml"] = "1"
|
|
288
|
+
if library and owner is None:
|
|
289
|
+
query["library"] = library
|
|
290
|
+
path = self._object_path(name_or_id, owner=owner, library=library)
|
|
291
|
+
return self._json_request("GET", self._with_query(path, query))
|
|
292
|
+
|
|
293
|
+
def signature(
|
|
294
|
+
self,
|
|
295
|
+
name_or_id: str,
|
|
296
|
+
*,
|
|
297
|
+
owner: str | None = None,
|
|
298
|
+
library: str | None = None,
|
|
299
|
+
version: int | None = None,
|
|
300
|
+
function: str | None = None,
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
return self._object_view(
|
|
303
|
+
name_or_id,
|
|
304
|
+
"signature",
|
|
305
|
+
owner=owner,
|
|
306
|
+
library=library,
|
|
307
|
+
version=version,
|
|
308
|
+
function=function,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def inputs(
|
|
312
|
+
self,
|
|
313
|
+
name_or_id: str,
|
|
314
|
+
*,
|
|
315
|
+
owner: str | None = None,
|
|
316
|
+
library: str | None = None,
|
|
317
|
+
version: int | None = None,
|
|
318
|
+
function: str | None = None,
|
|
319
|
+
) -> list[dict[str, Any]]:
|
|
320
|
+
return self._object_view(
|
|
321
|
+
name_or_id,
|
|
322
|
+
"inputs",
|
|
323
|
+
owner=owner,
|
|
324
|
+
library=library,
|
|
325
|
+
version=version,
|
|
326
|
+
function=function,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def outputs(
|
|
330
|
+
self,
|
|
331
|
+
name_or_id: str,
|
|
332
|
+
*,
|
|
333
|
+
owner: str | None = None,
|
|
334
|
+
library: str | None = None,
|
|
335
|
+
version: int | None = None,
|
|
336
|
+
function: str | None = None,
|
|
337
|
+
) -> list[dict[str, Any]]:
|
|
338
|
+
return self._object_view(
|
|
339
|
+
name_or_id,
|
|
340
|
+
"outputs",
|
|
341
|
+
owner=owner,
|
|
342
|
+
library=library,
|
|
343
|
+
version=version,
|
|
344
|
+
function=function,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def decomposition(
|
|
348
|
+
self,
|
|
349
|
+
name_or_id: str,
|
|
350
|
+
*,
|
|
351
|
+
owner: str | None = None,
|
|
352
|
+
library: str | None = None,
|
|
353
|
+
version: int | None = None,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
return self._object_view(
|
|
356
|
+
name_or_id,
|
|
357
|
+
"decomposition",
|
|
358
|
+
owner=owner,
|
|
359
|
+
library=library,
|
|
360
|
+
version=version,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def versions(
|
|
364
|
+
self,
|
|
365
|
+
name_or_id: str,
|
|
366
|
+
*,
|
|
367
|
+
owner: str | None = None,
|
|
368
|
+
library: str | None = None,
|
|
369
|
+
include_yaml: bool = False,
|
|
370
|
+
) -> list[dict[str, Any]]:
|
|
371
|
+
query: dict[str, Any] = {}
|
|
372
|
+
if include_yaml:
|
|
373
|
+
query["include_yaml"] = "1"
|
|
374
|
+
if library and owner is None:
|
|
375
|
+
query["library"] = library
|
|
376
|
+
path = f"{self._object_path(name_or_id, owner=owner, library=library)}/versions"
|
|
377
|
+
return self._json_request("GET", self._with_query(path, query))
|
|
378
|
+
|
|
379
|
+
def start(
|
|
380
|
+
self,
|
|
381
|
+
name: str,
|
|
382
|
+
*,
|
|
383
|
+
target_machine: str | None = None,
|
|
384
|
+
owner: str | None = None,
|
|
385
|
+
library: str | None = None,
|
|
386
|
+
args: list[Any] | None = None,
|
|
387
|
+
kwargs: dict[str, Any] | None = None,
|
|
388
|
+
output: str | None = None,
|
|
389
|
+
timeout_seconds: float | None = None,
|
|
390
|
+
version: int | None = None,
|
|
391
|
+
version_id: str | None = None,
|
|
392
|
+
function: str | None = None,
|
|
393
|
+
target_owner: str | None = None,
|
|
394
|
+
access_token: str | None = None,
|
|
395
|
+
correlation_id: str | None = None,
|
|
396
|
+
parent_run_id: str | None = None,
|
|
397
|
+
context: dict[str, Any] | None = None,
|
|
398
|
+
offline_policy: OfflinePolicy | None = None,
|
|
399
|
+
) -> ServerRemoteRun:
|
|
400
|
+
payload: dict[str, Any] = {"object": name}
|
|
401
|
+
if target_machine is not None:
|
|
402
|
+
payload["target_machine_id"] = target_machine
|
|
403
|
+
if target_owner is not None:
|
|
404
|
+
payload["target_owner_id"] = target_owner
|
|
405
|
+
if owner is not None:
|
|
406
|
+
payload["object_owner_id"] = owner
|
|
407
|
+
if library is not None:
|
|
408
|
+
payload["library"] = library
|
|
409
|
+
if args is not None:
|
|
410
|
+
payload["args"] = args
|
|
411
|
+
if kwargs is not None:
|
|
412
|
+
payload["kwargs"] = kwargs
|
|
413
|
+
if output is not None:
|
|
414
|
+
payload["output"] = output
|
|
415
|
+
if timeout_seconds is not None:
|
|
416
|
+
payload["timeout_seconds"] = timeout_seconds
|
|
417
|
+
if version is not None:
|
|
418
|
+
payload["version"] = int(version)
|
|
419
|
+
if version_id is not None:
|
|
420
|
+
payload["version_id"] = version_id
|
|
421
|
+
if function is not None:
|
|
422
|
+
payload["function"] = function
|
|
423
|
+
if access_token is not None:
|
|
424
|
+
payload["access_token"] = access_token
|
|
425
|
+
if correlation_id is not None:
|
|
426
|
+
payload["correlation_id"] = correlation_id
|
|
427
|
+
if parent_run_id is not None:
|
|
428
|
+
payload["parent_run_id"] = parent_run_id
|
|
429
|
+
if context:
|
|
430
|
+
payload["context"] = context
|
|
431
|
+
if offline_policy is not None:
|
|
432
|
+
payload["offline_policy"] = offline_policy
|
|
433
|
+
return ServerRemoteRun(self, self._json_request("POST", "/remote-runs", payload))
|
|
434
|
+
|
|
435
|
+
def call(
|
|
436
|
+
self,
|
|
437
|
+
name: str,
|
|
438
|
+
*,
|
|
439
|
+
target_machine: str | None = None,
|
|
440
|
+
owner: str | None = None,
|
|
441
|
+
library: str | None = None,
|
|
442
|
+
args: list[Any] | None = None,
|
|
443
|
+
kwargs: dict[str, Any] | None = None,
|
|
444
|
+
output: str | None = None,
|
|
445
|
+
timeout_seconds: float | None = None,
|
|
446
|
+
wait_timeout_seconds: float | None = None,
|
|
447
|
+
poll_interval: float = 0.5,
|
|
448
|
+
artifacts_dir: str | Path | None = None,
|
|
449
|
+
version: int | None = None,
|
|
450
|
+
version_id: str | None = None,
|
|
451
|
+
function: str | None = None,
|
|
452
|
+
target_owner: str | None = None,
|
|
453
|
+
access_token: str | None = None,
|
|
454
|
+
correlation_id: str | None = None,
|
|
455
|
+
parent_run_id: str | None = None,
|
|
456
|
+
context: dict[str, Any] | None = None,
|
|
457
|
+
offline_policy: OfflinePolicy | None = None,
|
|
458
|
+
) -> ServerCallResult:
|
|
459
|
+
run = self.start(
|
|
460
|
+
name,
|
|
461
|
+
target_machine=target_machine,
|
|
462
|
+
owner=owner,
|
|
463
|
+
library=library,
|
|
464
|
+
args=args,
|
|
465
|
+
kwargs=kwargs,
|
|
466
|
+
output=output,
|
|
467
|
+
timeout_seconds=timeout_seconds,
|
|
468
|
+
version=version,
|
|
469
|
+
version_id=version_id,
|
|
470
|
+
function=function,
|
|
471
|
+
target_owner=target_owner,
|
|
472
|
+
access_token=access_token,
|
|
473
|
+
correlation_id=correlation_id,
|
|
474
|
+
parent_run_id=parent_run_id,
|
|
475
|
+
context=context,
|
|
476
|
+
offline_policy=offline_policy,
|
|
477
|
+
)
|
|
478
|
+
return run.collect(
|
|
479
|
+
artifacts_dir=artifacts_dir,
|
|
480
|
+
poll_interval=poll_interval,
|
|
481
|
+
timeout_seconds=wait_timeout_seconds,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def runs(self, *, scope: RemoteRunScope | None = None) -> list[dict[str, Any]]:
|
|
485
|
+
query = {"scope": scope} if scope else {}
|
|
486
|
+
return self._json_request("GET", self._with_query("/remote-runs", query))
|
|
487
|
+
|
|
488
|
+
def list_runs(self, *, scope: RemoteRunScope | None = None) -> list[dict[str, Any]]:
|
|
489
|
+
return self.runs(scope=scope)
|
|
490
|
+
|
|
491
|
+
def get_run(self, run_id: str) -> dict[str, Any]:
|
|
492
|
+
return self._json_request("GET", f"/remote-runs/{_url_part(run_id)}")
|
|
493
|
+
|
|
494
|
+
def get_run_detail(self, run_id: str) -> dict[str, Any]:
|
|
495
|
+
return self._json_request("GET", f"/remote-runs/{_url_part(run_id)}/detail")
|
|
496
|
+
|
|
497
|
+
def list_events(self, run_id: str) -> list[dict[str, Any]]:
|
|
498
|
+
return self._json_request("GET", f"/remote-runs/{_url_part(run_id)}/events")
|
|
499
|
+
|
|
500
|
+
def list_artifacts(self, run_id: str) -> list[dict[str, Any]]:
|
|
501
|
+
return self._json_request("GET", f"/remote-runs/{_url_part(run_id)}/artifacts")
|
|
502
|
+
|
|
503
|
+
def artifact_bytes(self, run_id: str, name: str) -> bytes:
|
|
504
|
+
return self._bytes_request(
|
|
505
|
+
f"/remote-runs/{_url_part(run_id)}/artifacts/{_url_part(name)}"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def download_artifact(self, run_id: str, name: str, target: str | Path) -> Path:
|
|
509
|
+
target_path = Path(target)
|
|
510
|
+
if target_path.is_dir():
|
|
511
|
+
target_path = target_path / name
|
|
512
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
513
|
+
target_path.write_bytes(self.artifact_bytes(run_id, name))
|
|
514
|
+
return target_path
|
|
515
|
+
|
|
516
|
+
def cancel_run(self, run_id: str) -> dict[str, Any]:
|
|
517
|
+
return self._json_request("POST", f"/remote-runs/{_url_part(run_id)}/cancel")
|
|
518
|
+
|
|
519
|
+
def retry_run(self, run_id: str) -> ServerRemoteRun:
|
|
520
|
+
state = self._json_request("POST", f"/remote-runs/{_url_part(run_id)}/retry")
|
|
521
|
+
return ServerRemoteRun(self, state)
|
|
522
|
+
|
|
523
|
+
def _object_view(
|
|
524
|
+
self,
|
|
525
|
+
name_or_id: str,
|
|
526
|
+
suffix: str,
|
|
527
|
+
*,
|
|
528
|
+
owner: str | None,
|
|
529
|
+
library: str | None,
|
|
530
|
+
version: int | None,
|
|
531
|
+
function: str | None = None,
|
|
532
|
+
) -> Any:
|
|
533
|
+
query: dict[str, Any] = {}
|
|
534
|
+
if version is not None:
|
|
535
|
+
query["version"] = int(version)
|
|
536
|
+
if function is not None:
|
|
537
|
+
query["function"] = function
|
|
538
|
+
if library and owner is None:
|
|
539
|
+
query["library"] = library
|
|
540
|
+
path = f"{self._object_path(name_or_id, owner=owner, library=library)}/{suffix}"
|
|
541
|
+
return self._json_request("GET", self._with_query(path, query))
|
|
542
|
+
|
|
543
|
+
def _object_path(
|
|
544
|
+
self,
|
|
545
|
+
name_or_id: str,
|
|
546
|
+
*,
|
|
547
|
+
owner: str | None,
|
|
548
|
+
library: str | None,
|
|
549
|
+
) -> str:
|
|
550
|
+
if owner:
|
|
551
|
+
return (
|
|
552
|
+
f"/owners/{_url_part(owner)}/libraries/"
|
|
553
|
+
f"{_url_part(library or 'default')}/objects/{_url_part(name_or_id)}"
|
|
554
|
+
)
|
|
555
|
+
return f"/objects/{_url_part(name_or_id)}"
|
|
556
|
+
|
|
557
|
+
def _with_query(self, path: str, query: dict[str, Any]) -> str:
|
|
558
|
+
clean: list[tuple[str, Any]] = []
|
|
559
|
+
for key, value in query.items():
|
|
560
|
+
if value is None or value == "" or value is False:
|
|
561
|
+
continue
|
|
562
|
+
clean.append((key, "1" if value is True else value))
|
|
563
|
+
if not clean:
|
|
564
|
+
return path
|
|
565
|
+
return f"{path}?{urlencode(clean)}"
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class SPLExternalTokenClient:
|
|
569
|
+
"""Restricted direct client for external library execution tokens.
|
|
570
|
+
|
|
571
|
+
This facade intentionally exposes only callable-surface reads, remote-run
|
|
572
|
+
launch/read, events, and artifact download helpers. It does not expose
|
|
573
|
+
machine management, token management, grants, admin/settings, cancel, retry,
|
|
574
|
+
or broad object listing helpers.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
def __init__(
|
|
578
|
+
self,
|
|
579
|
+
token: str,
|
|
580
|
+
*,
|
|
581
|
+
base_url: str = DEFAULT_SERVER_URL,
|
|
582
|
+
):
|
|
583
|
+
self._client = SPLServerClient(token, base_url=base_url)
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def token(self) -> str:
|
|
587
|
+
return self._client.token
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def base_url(self) -> str:
|
|
591
|
+
return self._client.base_url
|
|
592
|
+
|
|
593
|
+
def signature(
|
|
594
|
+
self,
|
|
595
|
+
name_or_id: str,
|
|
596
|
+
*,
|
|
597
|
+
owner: str | None = None,
|
|
598
|
+
library: str | None = None,
|
|
599
|
+
version: int | None = None,
|
|
600
|
+
function: str | None = None,
|
|
601
|
+
) -> dict[str, Any]:
|
|
602
|
+
return self._client.signature(
|
|
603
|
+
name_or_id,
|
|
604
|
+
owner=owner,
|
|
605
|
+
library=library,
|
|
606
|
+
version=version,
|
|
607
|
+
function=function,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def inputs(
|
|
611
|
+
self,
|
|
612
|
+
name_or_id: str,
|
|
613
|
+
*,
|
|
614
|
+
owner: str | None = None,
|
|
615
|
+
library: str | None = None,
|
|
616
|
+
version: int | None = None,
|
|
617
|
+
function: str | None = None,
|
|
618
|
+
) -> list[dict[str, Any]]:
|
|
619
|
+
return self._client.inputs(
|
|
620
|
+
name_or_id,
|
|
621
|
+
owner=owner,
|
|
622
|
+
library=library,
|
|
623
|
+
version=version,
|
|
624
|
+
function=function,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def outputs(
|
|
628
|
+
self,
|
|
629
|
+
name_or_id: str,
|
|
630
|
+
*,
|
|
631
|
+
owner: str | None = None,
|
|
632
|
+
library: str | None = None,
|
|
633
|
+
version: int | None = None,
|
|
634
|
+
function: str | None = None,
|
|
635
|
+
) -> list[dict[str, Any]]:
|
|
636
|
+
return self._client.outputs(
|
|
637
|
+
name_or_id,
|
|
638
|
+
owner=owner,
|
|
639
|
+
library=library,
|
|
640
|
+
version=version,
|
|
641
|
+
function=function,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def decomposition(
|
|
645
|
+
self,
|
|
646
|
+
name_or_id: str,
|
|
647
|
+
*,
|
|
648
|
+
owner: str | None = None,
|
|
649
|
+
library: str | None = None,
|
|
650
|
+
version: int | None = None,
|
|
651
|
+
) -> dict[str, Any]:
|
|
652
|
+
return self._client.decomposition(
|
|
653
|
+
name_or_id,
|
|
654
|
+
owner=owner,
|
|
655
|
+
library=library,
|
|
656
|
+
version=version,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def get_object(
|
|
660
|
+
self,
|
|
661
|
+
name_or_id: str,
|
|
662
|
+
*,
|
|
663
|
+
owner: str | None = None,
|
|
664
|
+
library: str | None = None,
|
|
665
|
+
version: int | None = None,
|
|
666
|
+
) -> dict[str, Any]:
|
|
667
|
+
return self._client.get_object(
|
|
668
|
+
name_or_id,
|
|
669
|
+
owner=owner,
|
|
670
|
+
library=library,
|
|
671
|
+
version=version,
|
|
672
|
+
include_yaml=False,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def versions(
|
|
676
|
+
self,
|
|
677
|
+
name_or_id: str,
|
|
678
|
+
*,
|
|
679
|
+
owner: str | None = None,
|
|
680
|
+
library: str | None = None,
|
|
681
|
+
) -> list[dict[str, Any]]:
|
|
682
|
+
return self._client.versions(
|
|
683
|
+
name_or_id,
|
|
684
|
+
owner=owner,
|
|
685
|
+
library=library,
|
|
686
|
+
include_yaml=False,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def start(
|
|
690
|
+
self,
|
|
691
|
+
name: str,
|
|
692
|
+
*,
|
|
693
|
+
target_machine: str | None = None,
|
|
694
|
+
owner: str | None = None,
|
|
695
|
+
library: str | None = None,
|
|
696
|
+
args: list[Any] | None = None,
|
|
697
|
+
kwargs: dict[str, Any] | None = None,
|
|
698
|
+
output: str | None = None,
|
|
699
|
+
timeout_seconds: float | None = None,
|
|
700
|
+
version: int | None = None,
|
|
701
|
+
version_id: str | None = None,
|
|
702
|
+
function: str | None = None,
|
|
703
|
+
correlation_id: str | None = None,
|
|
704
|
+
context: dict[str, Any] | None = None,
|
|
705
|
+
offline_policy: OfflinePolicy | None = None,
|
|
706
|
+
) -> ServerRemoteRun:
|
|
707
|
+
return self._client.start(
|
|
708
|
+
name,
|
|
709
|
+
target_machine=target_machine,
|
|
710
|
+
owner=owner,
|
|
711
|
+
library=library,
|
|
712
|
+
args=args,
|
|
713
|
+
kwargs=kwargs,
|
|
714
|
+
output=output,
|
|
715
|
+
timeout_seconds=timeout_seconds,
|
|
716
|
+
version=version,
|
|
717
|
+
version_id=version_id,
|
|
718
|
+
function=function,
|
|
719
|
+
correlation_id=correlation_id,
|
|
720
|
+
context=context,
|
|
721
|
+
offline_policy=offline_policy,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def call(
|
|
725
|
+
self,
|
|
726
|
+
name: str,
|
|
727
|
+
*,
|
|
728
|
+
target_machine: str | None = None,
|
|
729
|
+
owner: str | None = None,
|
|
730
|
+
library: str | None = None,
|
|
731
|
+
args: list[Any] | None = None,
|
|
732
|
+
kwargs: dict[str, Any] | None = None,
|
|
733
|
+
output: str | None = None,
|
|
734
|
+
timeout_seconds: float | None = None,
|
|
735
|
+
wait_timeout_seconds: float | None = None,
|
|
736
|
+
poll_interval: float = 0.5,
|
|
737
|
+
artifacts_dir: str | Path | None = None,
|
|
738
|
+
version: int | None = None,
|
|
739
|
+
version_id: str | None = None,
|
|
740
|
+
function: str | None = None,
|
|
741
|
+
correlation_id: str | None = None,
|
|
742
|
+
context: dict[str, Any] | None = None,
|
|
743
|
+
offline_policy: OfflinePolicy | None = None,
|
|
744
|
+
) -> ServerCallResult:
|
|
745
|
+
run = self.start(
|
|
746
|
+
name,
|
|
747
|
+
target_machine=target_machine,
|
|
748
|
+
owner=owner,
|
|
749
|
+
library=library,
|
|
750
|
+
args=args,
|
|
751
|
+
kwargs=kwargs,
|
|
752
|
+
output=output,
|
|
753
|
+
timeout_seconds=timeout_seconds,
|
|
754
|
+
version=version,
|
|
755
|
+
version_id=version_id,
|
|
756
|
+
function=function,
|
|
757
|
+
correlation_id=correlation_id,
|
|
758
|
+
context=context,
|
|
759
|
+
offline_policy=offline_policy,
|
|
760
|
+
)
|
|
761
|
+
return run.collect(
|
|
762
|
+
artifacts_dir=artifacts_dir,
|
|
763
|
+
poll_interval=poll_interval,
|
|
764
|
+
timeout_seconds=wait_timeout_seconds,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
def get_run(self, run_id: str) -> dict[str, Any]:
|
|
768
|
+
return self._client.get_run(run_id)
|
|
769
|
+
|
|
770
|
+
def get_run_detail(self, run_id: str) -> dict[str, Any]:
|
|
771
|
+
return self._client.get_run_detail(run_id)
|
|
772
|
+
|
|
773
|
+
def list_events(self, run_id: str) -> list[dict[str, Any]]:
|
|
774
|
+
return self._client.list_events(run_id)
|
|
775
|
+
|
|
776
|
+
def list_artifacts(self, run_id: str) -> list[dict[str, Any]]:
|
|
777
|
+
return self._client.list_artifacts(run_id)
|
|
778
|
+
|
|
779
|
+
def artifact_bytes(self, run_id: str, name: str) -> bytes:
|
|
780
|
+
return self._client.artifact_bytes(run_id, name)
|
|
781
|
+
|
|
782
|
+
def download_artifact(self, run_id: str, name: str, target: str | Path) -> Path:
|
|
783
|
+
return self._client.download_artifact(run_id, name, target)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
ServerClient = SPLServerClient
|
|
787
|
+
ExternalExecutionClient = SPLExternalTokenClient
|