splime 0.2.3__tar.gz → 0.2.5__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 (87) hide show
  1. {splime-0.2.3/src/splime.egg-info → splime-0.2.5}/PKG-INFO +1 -1
  2. {splime-0.2.3 → splime-0.2.5}/pyproject.toml +1 -1
  3. {splime-0.2.3 → splime-0.2.5}/src/spl/_client.py +135 -65
  4. splime-0.2.5/src/spl/_views.py +685 -0
  5. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/remote_client.py +49 -10
  6. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/libraries.py +6 -2
  7. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/runtime_dependencies.py +1 -1
  8. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/server.py +1 -1
  9. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/worker.py +180 -16
  10. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon_client.py +7 -2
  11. {splime-0.2.3 → splime-0.2.5}/src/spl/pipeline_widget.py +16 -1
  12. {splime-0.2.3 → splime-0.2.5}/src/spl/server_client.py +140 -45
  13. {splime-0.2.3 → splime-0.2.5/src/splime.egg-info}/PKG-INFO +1 -1
  14. {splime-0.2.3 → splime-0.2.5}/src/splime.egg-info/SOURCES.txt +1 -0
  15. {splime-0.2.3 → splime-0.2.5}/LICENSE +0 -0
  16. {splime-0.2.3 → splime-0.2.5}/NOTICE +0 -0
  17. {splime-0.2.3 → splime-0.2.5}/README.md +0 -0
  18. {splime-0.2.3 → splime-0.2.5}/setup.cfg +0 -0
  19. {splime-0.2.3 → splime-0.2.5}/src/spl/__init__.py +0 -0
  20. {splime-0.2.3 → splime-0.2.5}/src/spl/_deprecate.py +0 -0
  21. {splime-0.2.3 → splime-0.2.5}/src/spl/_http.py +0 -0
  22. {splime-0.2.3 → splime-0.2.5}/src/spl/client.py +0 -0
  23. {splime-0.2.3 → splime-0.2.5}/src/spl/core/__init__.py +0 -0
  24. {splime-0.2.3 → splime-0.2.5}/src/spl/core/_common.py +0 -0
  25. {splime-0.2.3 → splime-0.2.5}/src/spl/core/common.py +0 -0
  26. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/__init__.py +0 -0
  27. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/adapter.py +0 -0
  28. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/artifact.py +0 -0
  29. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/control.py +0 -0
  30. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/distribution.py +0 -0
  31. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/function.py +0 -0
  32. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/local_function.py +0 -0
  33. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/misc.py +0 -0
  34. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/module.py +0 -0
  35. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/node.py +0 -0
  36. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/node_function.py +0 -0
  37. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/node_remote.py +0 -0
  38. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/pipeline.py +0 -0
  39. {splime-0.2.3 → splime-0.2.5}/src/spl/core/entities/scalar.py +0 -0
  40. {splime-0.2.3 → splime-0.2.5}/src/spl/core/ir/__init__.py +0 -0
  41. {splime-0.2.3 → splime-0.2.5}/src/spl/core/ir/common.py +0 -0
  42. {splime-0.2.3 → splime-0.2.5}/src/spl/core/ir/parse.py +0 -0
  43. {splime-0.2.3 → splime-0.2.5}/src/spl/core/ir/unparse.py +0 -0
  44. {splime-0.2.3 → splime-0.2.5}/src/spl/core/ir/utils.py +0 -0
  45. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/__init__.py +0 -0
  46. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/__main__.py +0 -0
  47. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/canonical.py +0 -0
  48. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/cli.py +0 -0
  49. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/client.py +0 -0
  50. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/docker_environment.py +0 -0
  51. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/docker_pool.py +0 -0
  52. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/doctor.py +0 -0
  53. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/environment.py +0 -0
  54. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/environment_base.py +0 -0
  55. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/heartbeat_service.py +0 -0
  56. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/metadata.py +0 -0
  57. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/__init__.py +0 -0
  58. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/env.py +0 -0
  59. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/library.py +0 -0
  60. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/object.py +0 -0
  61. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/run.py +0 -0
  62. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/server_connection.py +0 -0
  63. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/repositories/sync_event.py +0 -0
  64. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/__init__.py +0 -0
  65. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/_helpers.py +0 -0
  66. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/artifacts.py +0 -0
  67. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/diagnostics.py +0 -0
  68. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/envs.py +0 -0
  69. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/objects.py +0 -0
  70. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/remote.py +0 -0
  71. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/runs.py +0 -0
  72. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/routes/server_connections.py +0 -0
  73. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/run_progress.py +0 -0
  74. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/runtime_backend.py +0 -0
  75. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/runtime_config.py +0 -0
  76. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/secret_store.py +0 -0
  77. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/server_connection.py +0 -0
  78. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/services/__init__.py +0 -0
  79. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/services/sync.py +0 -0
  80. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/signature.py +0 -0
  81. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/storage_base.py +0 -0
  82. {splime-0.2.3 → splime-0.2.5}/src/spl/daemon/store.py +0 -0
  83. {splime-0.2.3 → splime-0.2.5}/src/spl/py.typed +0 -0
  84. {splime-0.2.3 → splime-0.2.5}/src/splime.egg-info/dependency_links.txt +0 -0
  85. {splime-0.2.3 → splime-0.2.5}/src/splime.egg-info/entry_points.txt +0 -0
  86. {splime-0.2.3 → splime-0.2.5}/src/splime.egg-info/requires.txt +0 -0
  87. {splime-0.2.3 → splime-0.2.5}/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.3
3
+ Version: 0.2.5
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.2.3"
7
+ version = "0.2.5"
8
8
 
9
9
  description = "Reuse Python functions across projects without rewriting or redeploying them."
10
10
  readme = "README.md"
@@ -23,6 +23,21 @@ from html import escape
23
23
  from pathlib import Path
24
24
  from typing import Any, Literal, cast, overload
25
25
 
26
+ from spl._views import (
27
+ ConnectionStatusView,
28
+ DecompositionView,
29
+ EnvTableView,
30
+ EnvironmentBuildListView,
31
+ HealthView,
32
+ InputListView,
33
+ LibraryListView,
34
+ MachineListView,
35
+ OutputListView,
36
+ RunListView,
37
+ RunRecordView,
38
+ SignatureView,
39
+ wrap_action,
40
+ )
26
41
  from spl.core.entities.node import DEFAULT_PORT
27
42
  from spl.daemon_client import (
28
43
  DEFAULT_DAEMON_HOST,
@@ -40,6 +55,11 @@ ObjectScope = Literal["auto", "local", "server", "all"]
40
55
  RunSource = Literal["auto", "local"]
41
56
  ProgressOption = bool | RunStateCallback
42
57
  _NO_SERVER_CONNECTION_MESSAGE = "active server connection is not found"
58
+ _LIBRARY_DELETE_UNSUPPORTED_MESSAGE = (
59
+ "Deleting central-server libraries is not supported by the SPL server API. "
60
+ "Use the Console archive action to hide a library, or remove individual "
61
+ "entries with client.library.remove_entry()."
62
+ )
43
63
 
44
64
 
45
65
  def _preview(value: Any, *, limit: int = 80) -> str:
@@ -348,7 +368,7 @@ class RemoteRun:
348
368
  server_side: bool = False,
349
369
  ):
350
370
  self._client = client
351
- self.state = state
371
+ self.state = RunRecordView(state)
352
372
  self.server_side = server_side
353
373
 
354
374
  @property
@@ -373,9 +393,9 @@ class RemoteRun:
373
393
  """Refresh and return the run state from the daemon."""
374
394
 
375
395
  if self.server_side:
376
- self.state = self._client._daemon.get_remote_run(self.id)
396
+ self.state = RunRecordView(self._client._daemon.get_remote_run(self.id))
377
397
  else:
378
- self.state = self._client._daemon.get_run(self.id)
398
+ self.state = RunRecordView(self._client._daemon.get_run(self.id))
379
399
  return self.state
380
400
 
381
401
  def wait(
@@ -395,18 +415,22 @@ class RemoteRun:
395
415
 
396
416
  on_state = _progress_callback(progress)
397
417
  if self.server_side:
398
- self.state = self._client._daemon.wait_remote_run(
399
- self.id,
400
- poll_interval=poll_interval,
401
- timeout_seconds=timeout_seconds,
402
- on_state=on_state,
418
+ self.state = RunRecordView(
419
+ self._client._daemon.wait_remote_run(
420
+ self.id,
421
+ poll_interval=poll_interval,
422
+ timeout_seconds=timeout_seconds,
423
+ on_state=on_state,
424
+ )
403
425
  )
404
426
  else:
405
- self.state = self._client._daemon.wait_run(
406
- self.id,
407
- poll_interval=poll_interval,
408
- timeout_seconds=timeout_seconds,
409
- on_state=on_state,
427
+ self.state = RunRecordView(
428
+ self._client._daemon.wait_run(
429
+ self.id,
430
+ poll_interval=poll_interval,
431
+ timeout_seconds=timeout_seconds,
432
+ on_state=on_state,
433
+ )
410
434
  )
411
435
  return self.state
412
436
 
@@ -517,13 +541,16 @@ class _LibraryAdmin:
517
541
  payload["default_machine_id"] = default_machine
518
542
  if execution is not None:
519
543
  payload["execution"] = execution
520
- return self._c._daemon.create_server_library(payload)
544
+ return wrap_action(
545
+ self._c._daemon.create_server_library(payload),
546
+ "library created",
547
+ )
521
548
 
522
549
  def get(self, ref: str) -> dict[str, Any]:
523
550
  """Return one central-server library by slug or id."""
524
551
 
525
552
  self._c._require_server_connection("reading a library")
526
- return self._c._daemon.get_server_library(ref)
553
+ return wrap_action(self._c._daemon.get_server_library(ref), "library")
527
554
 
528
555
  def update(
529
556
  self,
@@ -549,13 +576,16 @@ class _LibraryAdmin:
549
576
  payload["default_machine_id"] = default_machine
550
577
  if execution is not None:
551
578
  payload["execution"] = execution
552
- return self._c._daemon.update_server_library(ref, payload)
579
+ return wrap_action(
580
+ self._c._daemon.update_server_library(ref, payload),
581
+ "library updated",
582
+ )
553
583
 
554
584
  def delete(self, ref: str) -> dict[str, Any]:
555
- """Delete or archive one central-server library when supported upstream."""
585
+ """Raise a clear error because server-side library delete is unsupported."""
556
586
 
557
587
  self._c._require_server_connection("deleting a library")
558
- return self._c._daemon.delete_server_library(ref)
588
+ raise NotImplementedError(_LIBRARY_DELETE_UNSUPPORTED_MESSAGE)
559
589
 
560
590
  def grant(
561
591
  self,
@@ -574,13 +604,19 @@ class _LibraryAdmin:
574
604
  }
575
605
  if scopes is not None:
576
606
  payload["scopes"] = scopes
577
- return self._c._daemon.grant_server_library(ref, payload)
607
+ return wrap_action(
608
+ self._c._daemon.grant_server_library(ref, payload),
609
+ "library grant",
610
+ )
578
611
 
579
612
  def revoke(self, ref: str, grantee: str) -> dict[str, Any]:
580
613
  """Revoke a grantee's access to one central-server library."""
581
614
 
582
615
  self._c._require_server_connection("revoking library access")
583
- return self._c._daemon.revoke_server_library_grant(ref, grantee)
616
+ return wrap_action(
617
+ self._c._daemon.revoke_server_library_grant(ref, grantee),
618
+ "library grant revoked",
619
+ )
584
620
 
585
621
  def add_reference(
586
622
  self,
@@ -605,7 +641,10 @@ class _LibraryAdmin:
605
641
  payload["version"] = version
606
642
  if alias is not None:
607
643
  payload["alias"] = alias
608
- return self._c._daemon.add_server_library_reference(into_library, payload)
644
+ return wrap_action(
645
+ self._c._daemon.add_server_library_reference(into_library, payload),
646
+ "library reference",
647
+ )
609
648
 
610
649
  def copy_object(
611
650
  self,
@@ -630,13 +669,19 @@ class _LibraryAdmin:
630
669
  payload["version"] = version
631
670
  if new_name is not None:
632
671
  payload["new_name"] = new_name
633
- return self._c._daemon.copy_server_library_object(into_library, payload)
672
+ return wrap_action(
673
+ self._c._daemon.copy_server_library_object(into_library, payload),
674
+ "library copy",
675
+ )
634
676
 
635
677
  def remove_entry(self, library: str, name: str) -> dict[str, Any]:
636
678
  """Remove an owned object or reference entry from a central-server library."""
637
679
 
638
680
  self._c._require_server_connection("removing a library entry")
639
- return self._c._daemon.remove_server_library_entry(library, name)
681
+ return wrap_action(
682
+ self._c._daemon.remove_server_library_entry(library, name),
683
+ "library entry removed",
684
+ )
640
685
 
641
686
 
642
687
  class SPLClient:
@@ -683,7 +728,7 @@ class SPLClient:
683
728
  def health(self) -> dict[str, Any]:
684
729
  """Check that the local daemon is reachable."""
685
730
 
686
- return self._daemon.health()
731
+ return HealthView(self._daemon.health())
687
732
 
688
733
  def connect_server(
689
734
  self,
@@ -713,7 +758,7 @@ class SPLClient:
713
758
  )
714
759
  self._user_token = user_token
715
760
  self.server_connection = connection
716
- return self.server_connection
761
+ return ConnectionStatusView(self.server_connection)
717
762
 
718
763
  def disconnect_server(self) -> dict[str, Any]:
719
764
  """Gracefully disconnect the local daemon from the central server."""
@@ -755,24 +800,28 @@ class SPLClient:
755
800
  state = self._daemon.server_connection()
756
801
  except Exception as exc:
757
802
  if _is_missing_server_connection(exc):
758
- return {"connected": False, "offline": False, "connection": None}
803
+ return ConnectionStatusView(
804
+ {"connected": False, "offline": False, "connection": None}
805
+ )
759
806
  raise
760
807
  state.setdefault("connected", bool(state.get("server_url")))
761
- return state
808
+ return ConnectionStatusView(state)
762
809
 
763
810
  def machines(self) -> dict[str, Any]:
764
811
  """Return the user's machines, or an empty listing when not connected."""
765
812
 
766
813
  if not self._has_server_connection():
767
- return {"current_machine_id": None, "machines": []}
768
- return self._daemon.server_machines()
814
+ return MachineListView({"current_machine_id": None, "machines": []})
815
+ return MachineListView(self._daemon.server_machines())
769
816
 
770
817
  def libraries(self, *, include_accessible: bool = True) -> list[dict[str, Any]]:
771
818
  """Return visible libraries, or an empty list when not connected."""
772
819
 
773
820
  if not self._has_server_connection():
774
- return []
775
- return self._daemon.server_libraries(include_accessible=include_accessible)
821
+ return LibraryListView([])
822
+ return LibraryListView(
823
+ self._daemon.server_libraries(include_accessible=include_accessible)
824
+ )
776
825
 
777
826
  # The flat library aliases (create_library, get_library, update_library,
778
827
  # delete_library, grant_library, revoke_library_grant, add_reference,
@@ -790,7 +839,7 @@ class SPLClient:
790
839
  client.publish(my_function, env="default")
791
840
  """
792
841
 
793
- return self._daemon.register_env(name, python)
842
+ return wrap_action(self._daemon.register_env(name, python), "env registered")
794
843
 
795
844
  def publish(
796
845
  self,
@@ -1016,7 +1065,10 @@ class SPLClient:
1016
1065
  ) -> dict[str, Any]:
1017
1066
  """Remove one local daemon object without requiring a server connection."""
1018
1067
 
1019
- return self._daemon.forget(name, owner_id=owner, library=library)
1068
+ return wrap_action(
1069
+ self._daemon.forget(name, owner_id=owner, library=library),
1070
+ "object forgotten",
1071
+ )
1020
1072
 
1021
1073
  def remove_local(
1022
1074
  self,
@@ -1039,11 +1091,14 @@ class SPLClient:
1039
1091
  ) -> dict[str, Any]:
1040
1092
  """Remove one local object version without contacting the server."""
1041
1093
 
1042
- return self._daemon.forget_version(
1043
- name,
1044
- version,
1045
- owner_id=owner,
1046
- library=library,
1094
+ return wrap_action(
1095
+ self._daemon.forget_version(
1096
+ name,
1097
+ version,
1098
+ owner_id=owner,
1099
+ library=library,
1100
+ ),
1101
+ "object version forgotten",
1047
1102
  )
1048
1103
 
1049
1104
  def prune_stale_mirrors(
@@ -1054,7 +1109,10 @@ class SPLClient:
1054
1109
  ) -> dict[str, Any]:
1055
1110
  """Remove locally cached server-origin mirror rows."""
1056
1111
 
1057
- return self._daemon.prune_stale_mirrors(owner_id=owner, library=library)
1112
+ return wrap_action(
1113
+ self._daemon.prune_stale_mirrors(owner_id=owner, library=library),
1114
+ "stale mirrors pruned",
1115
+ )
1058
1116
 
1059
1117
  def _has_server_connection(self) -> bool:
1060
1118
  if self.server_connection is not None:
@@ -1099,12 +1157,14 @@ class SPLClient:
1099
1157
  ) -> dict[str, Any]:
1100
1158
  """Return a concise call/read signature for one daemon object."""
1101
1159
 
1102
- return self._daemon.signature(
1103
- name,
1104
- version=version,
1105
- owner_id=owner,
1106
- library=library,
1107
- function=function,
1160
+ return SignatureView(
1161
+ self._daemon.signature(
1162
+ name,
1163
+ version=version,
1164
+ owner_id=owner,
1165
+ library=library,
1166
+ function=function,
1167
+ )
1108
1168
  )
1109
1169
 
1110
1170
  def inputs(
@@ -1118,12 +1178,14 @@ class SPLClient:
1118
1178
  ) -> list[dict[str, Any]]:
1119
1179
  """Return the inputs that can be passed through ``kwargs``."""
1120
1180
 
1121
- return self._daemon.inputs(
1122
- name,
1123
- version=version,
1124
- owner_id=owner,
1125
- library=library,
1126
- function=function,
1181
+ return InputListView(
1182
+ self._daemon.inputs(
1183
+ name,
1184
+ version=version,
1185
+ owner_id=owner,
1186
+ library=library,
1187
+ function=function,
1188
+ )
1127
1189
  )
1128
1190
 
1129
1191
  def outputs(
@@ -1137,12 +1199,14 @@ class SPLClient:
1137
1199
  ) -> list[dict[str, Any]]:
1138
1200
  """Return output selectors and how to read ``result.value``."""
1139
1201
 
1140
- return self._daemon.outputs(
1141
- name,
1142
- version=version,
1143
- owner_id=owner,
1144
- library=library,
1145
- function=function,
1202
+ return OutputListView(
1203
+ self._daemon.outputs(
1204
+ name,
1205
+ version=version,
1206
+ owner_id=owner,
1207
+ library=library,
1208
+ function=function,
1209
+ )
1146
1210
  )
1147
1211
 
1148
1212
  def decomposition(
@@ -1156,17 +1220,20 @@ class SPLClient:
1156
1220
  """Return normalized function/node/link metadata for one object."""
1157
1221
 
1158
1222
  if self._is_node_remote(name):
1159
- return self._remote_decomposition_response(name, version=version)["decomposition"]
1223
+ return DecompositionView(
1224
+ self._remote_decomposition_response(name, version=version)["decomposition"]
1225
+ )
1160
1226
  if owner is not None or library is not None:
1161
- return self._remote_decomposition_response(
1227
+ response = self._remote_decomposition_response(
1162
1228
  {
1163
1229
  "name": str(name),
1164
1230
  "version": version,
1165
1231
  "owner_id": owner,
1166
1232
  "library": library,
1167
1233
  }
1168
- )["decomposition"]
1169
- return self._daemon.decomposition(str(name), version=version)
1234
+ )
1235
+ return DecompositionView(response["decomposition"])
1236
+ return DecompositionView(self._daemon.decomposition(str(name), version=version))
1170
1237
 
1171
1238
  def pipeline_widget(
1172
1239
  self,
@@ -1359,12 +1426,12 @@ class SPLClient:
1359
1426
  def envs(self) -> dict[str, Any]:
1360
1427
  """Return registered daemon environments."""
1361
1428
 
1362
- return self._daemon.list_envs()
1429
+ return EnvTableView(self._daemon.list_envs())
1363
1430
 
1364
1431
  def environment_builds(self) -> list[dict[str, Any]]:
1365
1432
  """Return cached daemon venv builds."""
1366
1433
 
1367
- return self._daemon.list_environment_builds()
1434
+ return EnvironmentBuildListView(self._daemon.list_environment_builds())
1368
1435
 
1369
1436
  def rebuild_environment(
1370
1437
  self,
@@ -1374,12 +1441,15 @@ class SPLClient:
1374
1441
  ) -> dict[str, Any]:
1375
1442
  """Force a cached daemon venv build to be recreated."""
1376
1443
 
1377
- return self._daemon.rebuild_environment_build(spec_hash, wait=wait)
1444
+ return wrap_action(
1445
+ self._daemon.rebuild_environment_build(spec_hash, wait=wait),
1446
+ "environment build",
1447
+ )
1378
1448
 
1379
1449
  def runs(self) -> list[dict[str, Any]]:
1380
1450
  """Return known daemon runs, newest first."""
1381
1451
 
1382
- return self._daemon.list_runs()
1452
+ return RunListView(self._daemon.list_runs())
1383
1453
 
1384
1454
  def _start_run(
1385
1455
  self,