splime 0.3.0__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 (100) hide show
  1. {splime-0.3.0/src/splime.egg-info → splime-0.4.0}/PKG-INFO +1 -1
  2. {splime-0.3.0 → splime-0.4.0}/pyproject.toml +1 -1
  3. {splime-0.3.0 → splime-0.4.0}/src/spl/_client.py +150 -22
  4. {splime-0.3.0 → splime-0.4.0}/src/spl/_views.py +66 -4
  5. splime-0.4.0/src/spl/core/_common.py +1263 -0
  6. splime-0.4.0/src/spl/core/adapter_compat.py +459 -0
  7. splime-0.4.0/src/spl/core/entities/adapter.py +426 -0
  8. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/artifact.py +28 -4
  9. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/function.py +6 -0
  10. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/pipeline.py +201 -41
  11. splime-0.4.0/src/spl/core/fingerprint.py +201 -0
  12. {splime-0.3.0 → splime-0.4.0}/src/spl/core/ir/utils.py +3 -1
  13. splime-0.4.0/src/spl/core/manifest.py +1003 -0
  14. splime-0.4.0/src/spl/core/node_runtime.py +524 -0
  15. splime-0.4.0/src/spl/core/resume.py +324 -0
  16. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/canonical.py +33 -2
  17. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/cli.py +88 -4
  18. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/doctor.py +67 -0
  19. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/remote_client.py +3 -0
  20. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/object.py +4 -0
  21. splime-0.4.0/src/spl/daemon/repositories/run.py +580 -0
  22. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/runs.py +79 -2
  23. splime-0.4.0/src/spl/daemon/routes/server_connections.py +155 -0
  24. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/run_progress.py +36 -0
  25. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/runtime_dependencies.py +4 -0
  26. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/server.py +210 -28
  27. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/server_connection.py +50 -0
  28. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/spl_free_runner.py +43 -7
  29. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/storage_base.py +44 -12
  30. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/store.py +33 -0
  31. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/worker.py +280 -35
  32. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon_client.py +151 -0
  33. {splime-0.3.0 → splime-0.4.0/src/splime.egg-info}/PKG-INFO +1 -1
  34. {splime-0.3.0 → splime-0.4.0}/src/splime.egg-info/SOURCES.txt +5 -0
  35. splime-0.3.0/src/spl/core/_common.py +0 -331
  36. splime-0.3.0/src/spl/core/entities/adapter.py +0 -176
  37. splime-0.3.0/src/spl/daemon/repositories/run.py +0 -295
  38. splime-0.3.0/src/spl/daemon/routes/server_connections.py +0 -83
  39. {splime-0.3.0 → splime-0.4.0}/LICENSE +0 -0
  40. {splime-0.3.0 → splime-0.4.0}/NOTICE +0 -0
  41. {splime-0.3.0 → splime-0.4.0}/README.md +0 -0
  42. {splime-0.3.0 → splime-0.4.0}/setup.cfg +0 -0
  43. {splime-0.3.0 → splime-0.4.0}/src/spl/__init__.py +0 -0
  44. {splime-0.3.0 → splime-0.4.0}/src/spl/_deprecate.py +0 -0
  45. {splime-0.3.0 → splime-0.4.0}/src/spl/_http.py +0 -0
  46. {splime-0.3.0 → splime-0.4.0}/src/spl/client.py +0 -0
  47. {splime-0.3.0 → splime-0.4.0}/src/spl/core/__init__.py +0 -0
  48. {splime-0.3.0 → splime-0.4.0}/src/spl/core/common.py +0 -0
  49. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/__init__.py +0 -0
  50. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/control.py +0 -0
  51. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/distribution.py +0 -0
  52. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/local_function.py +0 -0
  53. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/misc.py +0 -0
  54. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/module.py +0 -0
  55. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/node.py +0 -0
  56. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/node_function.py +0 -0
  57. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/node_remote.py +0 -0
  58. {splime-0.3.0 → splime-0.4.0}/src/spl/core/entities/scalar.py +0 -0
  59. {splime-0.3.0 → splime-0.4.0}/src/spl/core/ir/__init__.py +0 -0
  60. {splime-0.3.0 → splime-0.4.0}/src/spl/core/ir/common.py +0 -0
  61. {splime-0.3.0 → splime-0.4.0}/src/spl/core/ir/parse.py +0 -0
  62. {splime-0.3.0 → splime-0.4.0}/src/spl/core/ir/unparse.py +0 -0
  63. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/__init__.py +0 -0
  64. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/__main__.py +0 -0
  65. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/client.py +0 -0
  66. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/docker_environment.py +0 -0
  67. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/docker_pool.py +0 -0
  68. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/environment.py +0 -0
  69. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/environment_base.py +0 -0
  70. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/heartbeat_service.py +0 -0
  71. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/interpreter_visibility.py +0 -0
  72. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/metadata.py +0 -0
  73. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/__init__.py +0 -0
  74. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/env.py +0 -0
  75. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/library.py +0 -0
  76. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/server_connection.py +0 -0
  77. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/repositories/sync_event.py +0 -0
  78. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/__init__.py +0 -0
  79. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/_helpers.py +0 -0
  80. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/artifacts.py +0 -0
  81. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/diagnostics.py +0 -0
  82. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/envs.py +0 -0
  83. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/libraries.py +0 -0
  84. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/objects.py +0 -0
  85. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/routes/remote.py +0 -0
  86. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/runtime_backend.py +0 -0
  87. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/runtime_config.py +0 -0
  88. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/secret_store.py +0 -0
  89. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/services/__init__.py +0 -0
  90. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/services/sync.py +0 -0
  91. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/signature.py +0 -0
  92. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/spl_free_generator.py +0 -0
  93. {splime-0.3.0 → splime-0.4.0}/src/spl/daemon/worker_runtime_marker.py +0 -0
  94. {splime-0.3.0 → splime-0.4.0}/src/spl/pipeline_widget.py +0 -0
  95. {splime-0.3.0 → splime-0.4.0}/src/spl/py.typed +0 -0
  96. {splime-0.3.0 → splime-0.4.0}/src/spl/server_client.py +0 -0
  97. {splime-0.3.0 → splime-0.4.0}/src/splime.egg-info/dependency_links.txt +0 -0
  98. {splime-0.3.0 → splime-0.4.0}/src/splime.egg-info/entry_points.txt +0 -0
  99. {splime-0.3.0 → splime-0.4.0}/src/splime.egg-info/requires.txt +0 -0
  100. {splime-0.3.0 → 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.3.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splime"
7
- version = "0.3.0"
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"
@@ -39,6 +39,7 @@ from spl._views import (
39
39
  SignatureView,
40
40
  wrap_action,
41
41
  )
42
+ from spl.core import manifest as m_manifest
42
43
  from spl.core.entities.node import DEFAULT_PORT
43
44
  from spl.daemon_client import (
44
45
  DEFAULT_DAEMON_HOST,
@@ -71,7 +72,8 @@ def _preview(value: Any, *, limit: int = 80) -> str:
71
72
 
72
73
 
73
74
  def _is_missing_server_connection(exc: Exception) -> bool:
74
- return isinstance(exc, ClientError) and _NO_SERVER_CONNECTION_MESSAGE in str(exc)
75
+ message = str(exc)
76
+ return isinstance(exc, ClientError) and _NO_SERVER_CONNECTION_MESSAGE in message
75
77
 
76
78
 
77
79
  def _progress_callback(progress: ProgressOption) -> RunStateCallback | None:
@@ -767,11 +769,29 @@ class SPLClient:
767
769
  return ConnectionStatusView(state)
768
770
 
769
771
  def machines(self) -> dict[str, Any]:
770
- """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.
771
773
 
772
- 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
773
793
  return MachineListView({"current_machine_id": None, "machines": []})
774
- return MachineListView(self._daemon.server_machines())
794
+ return MachineListView(machines)
775
795
 
776
796
  def libraries(self, *, include_accessible: bool = True) -> list[dict[str, Any]]:
777
797
  """Return visible libraries, or an empty list when not connected."""
@@ -1368,10 +1388,89 @@ class SPLClient:
1368
1388
  "environment build",
1369
1389
  )
1370
1390
 
1371
- def runs(self) -> list[dict[str, Any]]:
1391
+ def runs(self, *, local: bool = False) -> list[dict[str, Any]]:
1372
1392
  """Return known daemon runs, newest first."""
1373
1393
 
1374
- 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")
1375
1474
 
1376
1475
  def _start_run(
1377
1476
  self,
@@ -1387,24 +1486,37 @@ class SPLClient:
1387
1486
  offline_policy: OfflinePolicy | None = None,
1388
1487
  function: str | None = None,
1389
1488
  source: RunSource = "auto",
1489
+ adapters: Any | None = None,
1490
+ runtimes: dict[str, str] | None = None,
1491
+ keep: bool | str | None = None,
1390
1492
  ) -> RemoteRun:
1391
1493
  """Shared implementation behind ``submit``/``call`` (and legacy aliases)."""
1392
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
+
1393
1501
  remote = target_machine is not None or owner is not None or library is not None
1394
- state = self._daemon.run(
1395
- name,
1396
- args=args,
1397
- kwargs=kwargs,
1398
- output=output,
1399
- timeout_seconds=timeout_seconds,
1400
- target_machine=target_machine,
1401
- object_owner_id=owner,
1402
- library=library,
1403
- offline_policy=offline_policy,
1404
- function=function,
1405
- source=source,
1406
- remote=remote or None,
1407
- )
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)
1408
1520
  return RemoteRun(self, state, server_side=remote)
1409
1521
 
1410
1522
  # ``start()`` warned through 0.1.4/0.1.5 and was removed in 0.2.0 — use
@@ -1424,12 +1536,17 @@ class SPLClient:
1424
1536
  offline_policy: OfflinePolicy | None = None,
1425
1537
  function: str | None = None,
1426
1538
  source: RunSource = "auto",
1539
+ adapters: Any | None = None,
1540
+ runtimes: dict[str, str] | None = None,
1541
+ keep: bool | str | None = None,
1427
1542
  ) -> RemoteRun:
1428
1543
  """Canonical async entry point: start a run, return a handle immediately.
1429
1544
 
1430
1545
  The default path is local daemon execution. Passing ``target_machine``,
1431
1546
  ``owner``, or ``library`` intentionally selects central-server remote
1432
- 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.
1433
1550
  """
1434
1551
 
1435
1552
  return self._start_run(
@@ -1444,6 +1561,9 @@ class SPLClient:
1444
1561
  offline_policy=offline_policy,
1445
1562
  function=function,
1446
1563
  source=source,
1564
+ adapters=adapters,
1565
+ runtimes=runtimes,
1566
+ keep=keep,
1447
1567
  )
1448
1568
 
1449
1569
  # ``queue()`` warned through 0.1.4/0.1.5 and was removed in 0.2.0 — use
@@ -1464,6 +1584,9 @@ class SPLClient:
1464
1584
  offline_policy: OfflinePolicy | None = None,
1465
1585
  function: str | None = None,
1466
1586
  source: RunSource = "auto",
1587
+ adapters: Any | None = None,
1588
+ runtimes: dict[str, str] | None = None,
1589
+ keep: bool | str | None = None,
1467
1590
  progress: ProgressOption = True,
1468
1591
  ) -> RemoteResult:
1469
1592
  """Run an object, wait for completion, and return result/artifacts.
@@ -1476,7 +1599,9 @@ class SPLClient:
1476
1599
  While waiting, slow phases (a first-run environment build, a queued
1477
1600
  server-side run) print short progress lines to stderr. Pass
1478
1601
  ``progress=False`` to wait silently, or a callable to receive every
1479
- 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.
1480
1605
  """
1481
1606
 
1482
1607
  run = self._start_run(
@@ -1491,6 +1616,9 @@ class SPLClient:
1491
1616
  offline_policy=offline_policy,
1492
1617
  function=function,
1493
1618
  source=source,
1619
+ adapters=adapters,
1620
+ runtimes=runtimes,
1621
+ keep=keep,
1494
1622
  )
1495
1623
  return run.collect(
1496
1624
  artifacts_dir=artifacts_dir,
@@ -136,6 +136,20 @@ def _status(value: Any) -> str:
136
136
  return _EMPTY if value is None else str(value)
137
137
 
138
138
 
139
+ def _mapping_rows(value: Any) -> list[Mapping[str, Any]]:
140
+ if not isinstance(value, list):
141
+ return []
142
+ return [item for item in value if isinstance(item, Mapping)]
143
+
144
+
145
+ def _adapter_label(row: Mapping[str, Any]) -> str:
146
+ save = row.get("save")
147
+ load = row.get("load")
148
+ if save and load and save != load:
149
+ return "{} -> {}".format(save, load)
150
+ return _EMPTY if save is None and load is None else str(save or load)
151
+
152
+
139
153
  class CompactDict(dict[str, Any]):
140
154
  """A dict with compact ``repr`` and plain ``.raw`` access."""
141
155
 
@@ -516,6 +530,8 @@ class DecompositionView(CompactDict):
516
530
 
517
531
  class RunRecordView(CompactDict):
518
532
  title = "run"
533
+ runtime_headers = ("node", "runtime", "source")
534
+ edge_headers = ("edge", "tag", "adapter", "source")
519
535
 
520
536
  def _summary_rows(self) -> list[tuple[str, Any]]:
521
537
  result = self.get("result")
@@ -526,9 +542,13 @@ class RunRecordView(CompactDict):
526
542
  return [
527
543
  ("id", self.get("id")),
528
544
  ("status", self.get("status")),
545
+ ("keep", self.get("keep")),
529
546
  ("mode", self.get("mode") or self.get("source")),
530
547
  ("object", self.get("object") or self.get("object_name")),
531
548
  ("output", self.get("output")),
549
+ ("parent", self.get("parent_run_id")),
550
+ ("manifest", self.get("has_manifest")),
551
+ ("disk bytes", self.get("disk_size_bytes")),
532
552
  ("created", self.get("created_at")),
533
553
  ("started", self.get("started_at")),
534
554
  ("finished", self.get("finished_at")),
@@ -536,10 +556,51 @@ class RunRecordView(CompactDict):
536
556
  ("error", self.get("error")),
537
557
  ]
538
558
 
559
+ def _runtime_rows(self) -> list[dict[str, Any]]:
560
+ return [
561
+ {
562
+ "node": item.get("alias") or short_id(item.get("node_id")),
563
+ "runtime": item.get("name"),
564
+ "source": item.get("source"),
565
+ }
566
+ for item in _mapping_rows(self.get("node_runtimes"))
567
+ ]
568
+
569
+ def _edge_rows(self) -> list[dict[str, Any]]:
570
+ return [
571
+ {
572
+ "edge": "{} -> {}".format(item.get("source") or _EMPTY, item.get("target") or _EMPTY),
573
+ "tag": item.get("tag"),
574
+ "adapter": _adapter_label(item),
575
+ "source": item.get("source_level"),
576
+ }
577
+ for item in _mapping_rows(self.get("edge_adapters"))
578
+ ]
579
+
580
+ def __repr__(self) -> str:
581
+ lines = [details_to_text(self._title, self._summary_rows())]
582
+ runtime_rows = self._runtime_rows()
583
+ edge_rows = self._edge_rows()
584
+ if runtime_rows:
585
+ lines.append(table_to_text("node runtimes", self.runtime_headers, runtime_rows))
586
+ if edge_rows:
587
+ lines.append(table_to_text("edge adapters", self.edge_headers, edge_rows))
588
+ return "\n\n".join(lines)
589
+
590
+ def _repr_html_(self) -> str:
591
+ parts = [details_to_html(self._title, self._summary_rows())]
592
+ runtime_rows = self._runtime_rows()
593
+ edge_rows = self._edge_rows()
594
+ if runtime_rows:
595
+ parts.append(table_to_html("node runtimes", self.runtime_headers, runtime_rows))
596
+ if edge_rows:
597
+ parts.append(table_to_html("edge adapters", self.edge_headers, edge_rows))
598
+ return "".join(parts)
599
+
539
600
 
540
601
  class RunListView(CompactList):
541
602
  title = "runs"
542
- headers = ("id", "status", "mode", "object", "output", "created", "error")
603
+ headers = ("id", "status", "keep", "manifest", "parent", "size", "object", "created")
543
604
 
544
605
  def __init__(self, payload: list[Any] | None = None, *, title: str | None = None):
545
606
  super().__init__(
@@ -556,11 +617,12 @@ class RunListView(CompactList):
556
617
  {
557
618
  "id": short_id(item.get("id")),
558
619
  "status": _status(item.get("status")),
559
- "mode": item.get("mode") or item.get("source"),
620
+ "keep": item.get("keep"),
621
+ "manifest": "yes" if item.get("has_manifest") else "no",
622
+ "parent": short_id(item.get("parent_run_id")),
623
+ "size": item.get("disk_size_bytes"),
560
624
  "object": item.get("object") or item.get("object_name"),
561
- "output": item.get("output"),
562
625
  "created": item.get("created_at"),
563
- "error": item.get("error"),
564
626
  }
565
627
  for item in self
566
628
  if isinstance(item, Mapping)