py-data-engine 0.1.0__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.
Files changed (200) hide show
  1. data_engine/__init__.py +37 -0
  2. data_engine/application/__init__.py +39 -0
  3. data_engine/application/actions.py +42 -0
  4. data_engine/application/catalog.py +151 -0
  5. data_engine/application/control.py +213 -0
  6. data_engine/application/details.py +73 -0
  7. data_engine/application/runtime.py +449 -0
  8. data_engine/application/workspace.py +62 -0
  9. data_engine/authoring/__init__.py +14 -0
  10. data_engine/authoring/builder.py +31 -0
  11. data_engine/authoring/execution/__init__.py +6 -0
  12. data_engine/authoring/execution/app.py +6 -0
  13. data_engine/authoring/execution/context.py +82 -0
  14. data_engine/authoring/execution/continuous.py +176 -0
  15. data_engine/authoring/execution/grouped.py +106 -0
  16. data_engine/authoring/execution/logging.py +83 -0
  17. data_engine/authoring/execution/polling.py +135 -0
  18. data_engine/authoring/execution/runner.py +210 -0
  19. data_engine/authoring/execution/single.py +171 -0
  20. data_engine/authoring/flow.py +361 -0
  21. data_engine/authoring/helpers.py +160 -0
  22. data_engine/authoring/model.py +59 -0
  23. data_engine/authoring/primitives.py +430 -0
  24. data_engine/authoring/services.py +42 -0
  25. data_engine/devtools/__init__.py +3 -0
  26. data_engine/devtools/project_ast_map.py +503 -0
  27. data_engine/docs/__init__.py +1 -0
  28. data_engine/docs/sphinx_source/_static/custom.css +13 -0
  29. data_engine/docs/sphinx_source/api.rst +42 -0
  30. data_engine/docs/sphinx_source/conf.py +37 -0
  31. data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
  32. data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
  33. data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
  34. data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
  35. data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
  36. data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
  37. data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
  38. data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
  39. data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
  40. data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
  41. data_engine/docs/sphinx_source/guides/project-map.md +118 -0
  42. data_engine/docs/sphinx_source/guides/recipes.md +268 -0
  43. data_engine/docs/sphinx_source/index.rst +22 -0
  44. data_engine/domain/__init__.py +92 -0
  45. data_engine/domain/actions.py +69 -0
  46. data_engine/domain/catalog.py +128 -0
  47. data_engine/domain/details.py +214 -0
  48. data_engine/domain/diagnostics.py +56 -0
  49. data_engine/domain/errors.py +104 -0
  50. data_engine/domain/inspection.py +99 -0
  51. data_engine/domain/logs.py +118 -0
  52. data_engine/domain/operations.py +172 -0
  53. data_engine/domain/operator.py +72 -0
  54. data_engine/domain/runs.py +155 -0
  55. data_engine/domain/runtime.py +279 -0
  56. data_engine/domain/source_state.py +17 -0
  57. data_engine/domain/support.py +54 -0
  58. data_engine/domain/time.py +23 -0
  59. data_engine/domain/workspace.py +159 -0
  60. data_engine/flow_modules/__init__.py +1 -0
  61. data_engine/flow_modules/flow_module_compiler.py +179 -0
  62. data_engine/flow_modules/flow_module_loader.py +201 -0
  63. data_engine/helpers/__init__.py +25 -0
  64. data_engine/helpers/duckdb.py +705 -0
  65. data_engine/hosts/__init__.py +1 -0
  66. data_engine/hosts/daemon/__init__.py +23 -0
  67. data_engine/hosts/daemon/app.py +221 -0
  68. data_engine/hosts/daemon/bootstrap.py +69 -0
  69. data_engine/hosts/daemon/client.py +465 -0
  70. data_engine/hosts/daemon/commands.py +64 -0
  71. data_engine/hosts/daemon/composition.py +310 -0
  72. data_engine/hosts/daemon/constants.py +15 -0
  73. data_engine/hosts/daemon/entrypoints.py +97 -0
  74. data_engine/hosts/daemon/lifecycle.py +191 -0
  75. data_engine/hosts/daemon/manager.py +272 -0
  76. data_engine/hosts/daemon/ownership.py +126 -0
  77. data_engine/hosts/daemon/runtime_commands.py +188 -0
  78. data_engine/hosts/daemon/runtime_control.py +31 -0
  79. data_engine/hosts/daemon/server.py +84 -0
  80. data_engine/hosts/daemon/shared_state.py +147 -0
  81. data_engine/hosts/daemon/state_sync.py +101 -0
  82. data_engine/platform/__init__.py +1 -0
  83. data_engine/platform/identity.py +35 -0
  84. data_engine/platform/local_settings.py +146 -0
  85. data_engine/platform/theme.py +259 -0
  86. data_engine/platform/workspace_models.py +190 -0
  87. data_engine/platform/workspace_policy.py +333 -0
  88. data_engine/runtime/__init__.py +1 -0
  89. data_engine/runtime/file_watch.py +185 -0
  90. data_engine/runtime/ledger_models.py +116 -0
  91. data_engine/runtime/runtime_db.py +938 -0
  92. data_engine/runtime/shared_state.py +523 -0
  93. data_engine/services/__init__.py +49 -0
  94. data_engine/services/daemon.py +64 -0
  95. data_engine/services/daemon_state.py +40 -0
  96. data_engine/services/flow_catalog.py +102 -0
  97. data_engine/services/flow_execution.py +48 -0
  98. data_engine/services/ledger.py +85 -0
  99. data_engine/services/logs.py +65 -0
  100. data_engine/services/runtime_binding.py +105 -0
  101. data_engine/services/runtime_execution.py +126 -0
  102. data_engine/services/runtime_history.py +62 -0
  103. data_engine/services/settings.py +58 -0
  104. data_engine/services/shared_state.py +28 -0
  105. data_engine/services/theme.py +59 -0
  106. data_engine/services/workspace_provisioning.py +224 -0
  107. data_engine/services/workspaces.py +74 -0
  108. data_engine/ui/__init__.py +3 -0
  109. data_engine/ui/cli/__init__.py +19 -0
  110. data_engine/ui/cli/app.py +161 -0
  111. data_engine/ui/cli/commands_doctor.py +178 -0
  112. data_engine/ui/cli/commands_run.py +80 -0
  113. data_engine/ui/cli/commands_start.py +100 -0
  114. data_engine/ui/cli/commands_workspace.py +97 -0
  115. data_engine/ui/cli/dependencies.py +44 -0
  116. data_engine/ui/cli/parser.py +56 -0
  117. data_engine/ui/gui/__init__.py +25 -0
  118. data_engine/ui/gui/app.py +116 -0
  119. data_engine/ui/gui/bootstrap.py +487 -0
  120. data_engine/ui/gui/bootstrapper.py +140 -0
  121. data_engine/ui/gui/cache_models.py +23 -0
  122. data_engine/ui/gui/control_support.py +185 -0
  123. data_engine/ui/gui/controllers/__init__.py +6 -0
  124. data_engine/ui/gui/controllers/flows.py +439 -0
  125. data_engine/ui/gui/controllers/runtime.py +245 -0
  126. data_engine/ui/gui/dialogs/__init__.py +12 -0
  127. data_engine/ui/gui/dialogs/messages.py +88 -0
  128. data_engine/ui/gui/dialogs/previews.py +222 -0
  129. data_engine/ui/gui/helpers/__init__.py +62 -0
  130. data_engine/ui/gui/helpers/inspection.py +81 -0
  131. data_engine/ui/gui/helpers/lifecycle.py +112 -0
  132. data_engine/ui/gui/helpers/scroll.py +28 -0
  133. data_engine/ui/gui/helpers/theming.py +87 -0
  134. data_engine/ui/gui/icons/dark_light.svg +12 -0
  135. data_engine/ui/gui/icons/documentation.svg +1 -0
  136. data_engine/ui/gui/icons/failed.svg +3 -0
  137. data_engine/ui/gui/icons/group.svg +4 -0
  138. data_engine/ui/gui/icons/home.svg +2 -0
  139. data_engine/ui/gui/icons/manual.svg +2 -0
  140. data_engine/ui/gui/icons/poll.svg +2 -0
  141. data_engine/ui/gui/icons/schedule.svg +4 -0
  142. data_engine/ui/gui/icons/settings.svg +2 -0
  143. data_engine/ui/gui/icons/started.svg +3 -0
  144. data_engine/ui/gui/icons/success.svg +3 -0
  145. data_engine/ui/gui/icons/view-log.svg +3 -0
  146. data_engine/ui/gui/icons.py +50 -0
  147. data_engine/ui/gui/launcher.py +48 -0
  148. data_engine/ui/gui/presenters/__init__.py +72 -0
  149. data_engine/ui/gui/presenters/docs.py +140 -0
  150. data_engine/ui/gui/presenters/logs.py +58 -0
  151. data_engine/ui/gui/presenters/runtime_projection.py +29 -0
  152. data_engine/ui/gui/presenters/sidebar.py +88 -0
  153. data_engine/ui/gui/presenters/steps.py +148 -0
  154. data_engine/ui/gui/presenters/workspace.py +39 -0
  155. data_engine/ui/gui/presenters/workspace_binding.py +75 -0
  156. data_engine/ui/gui/presenters/workspace_settings.py +182 -0
  157. data_engine/ui/gui/preview_models.py +37 -0
  158. data_engine/ui/gui/render_support.py +241 -0
  159. data_engine/ui/gui/rendering/__init__.py +12 -0
  160. data_engine/ui/gui/rendering/artifacts.py +95 -0
  161. data_engine/ui/gui/rendering/icons.py +50 -0
  162. data_engine/ui/gui/runtime.py +47 -0
  163. data_engine/ui/gui/state_support.py +193 -0
  164. data_engine/ui/gui/support.py +214 -0
  165. data_engine/ui/gui/surface.py +209 -0
  166. data_engine/ui/gui/theme.py +720 -0
  167. data_engine/ui/gui/widgets/__init__.py +34 -0
  168. data_engine/ui/gui/widgets/config.py +41 -0
  169. data_engine/ui/gui/widgets/logs.py +62 -0
  170. data_engine/ui/gui/widgets/panels.py +507 -0
  171. data_engine/ui/gui/widgets/sidebar.py +130 -0
  172. data_engine/ui/gui/widgets/steps.py +84 -0
  173. data_engine/ui/tui/__init__.py +5 -0
  174. data_engine/ui/tui/app.py +222 -0
  175. data_engine/ui/tui/bootstrap.py +475 -0
  176. data_engine/ui/tui/bootstrapper.py +117 -0
  177. data_engine/ui/tui/controllers/__init__.py +6 -0
  178. data_engine/ui/tui/controllers/flows.py +349 -0
  179. data_engine/ui/tui/controllers/runtime.py +167 -0
  180. data_engine/ui/tui/runtime.py +34 -0
  181. data_engine/ui/tui/state_support.py +141 -0
  182. data_engine/ui/tui/support.py +63 -0
  183. data_engine/ui/tui/theme.py +204 -0
  184. data_engine/ui/tui/widgets.py +123 -0
  185. data_engine/views/__init__.py +109 -0
  186. data_engine/views/actions.py +80 -0
  187. data_engine/views/artifacts.py +58 -0
  188. data_engine/views/flow_display.py +69 -0
  189. data_engine/views/logs.py +54 -0
  190. data_engine/views/models.py +96 -0
  191. data_engine/views/presentation.py +133 -0
  192. data_engine/views/runs.py +62 -0
  193. data_engine/views/state.py +39 -0
  194. data_engine/views/status.py +13 -0
  195. data_engine/views/text.py +109 -0
  196. py_data_engine-0.1.0.dist-info/METADATA +330 -0
  197. py_data_engine-0.1.0.dist-info/RECORD +200 -0
  198. py_data_engine-0.1.0.dist-info/WHEEL +5 -0
  199. py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
  200. py_data_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,37 @@
1
+ """Top-level package for the Data Engine workbook runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from data_engine.authoring.builder import Batch
9
+ from data_engine.authoring.builder import FileRef
10
+ from data_engine.authoring.builder import Flow
11
+ from data_engine.authoring.builder import FlowContext
12
+ from data_engine.authoring.builder import discover_flows, load_flow, run
13
+
14
+ __all__ = ["Batch", "FileRef", "Flow", "FlowContext", "discover_flows", "load_flow", "run"]
15
+
16
+
17
+ def __getattr__(name: str):
18
+ """Lazy-load runtime symbols so lightweight helpers can import package submodules safely."""
19
+ if name in {"Batch", "FileRef", "Flow", "FlowContext", "discover_flows", "load_flow", "run"}:
20
+ from data_engine.authoring.builder import Batch
21
+ from data_engine.authoring.builder import FileRef
22
+ from data_engine.authoring.builder import Flow
23
+ from data_engine.authoring.builder import FlowContext
24
+ from data_engine.authoring.builder import discover_flows
25
+ from data_engine.authoring.builder import load_flow
26
+ from data_engine.authoring.builder import run
27
+
28
+ return {
29
+ "Batch": Batch,
30
+ "FileRef": FileRef,
31
+ "Flow": Flow,
32
+ "FlowContext": FlowContext,
33
+ "discover_flows": discover_flows,
34
+ "load_flow": load_flow,
35
+ "run": run,
36
+ }[name]
37
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,39 @@
1
+ """Host-agnostic application use cases built on top of services and domain models."""
2
+
3
+ from data_engine.application.actions import ActionStateApplication
4
+ from data_engine.application.catalog import FlowCatalogApplication, FlowCatalogLoadResult, FlowCatalogPresentation
5
+ from data_engine.application.control import FlowRefreshResult, OperatorActionResult, OperatorControlApplication
6
+ from data_engine.application.details import DetailApplication, SelectedFlowPresentation
7
+ from data_engine.application.runtime import (
8
+ DaemonCommandResult,
9
+ EngineRunCompletion,
10
+ FlowStateRefreshPlan,
11
+ ManualRunCompletion,
12
+ RuntimeApplication,
13
+ RuntimeLogMessage,
14
+ RuntimeSnapshotPresentation,
15
+ RuntimeSyncState,
16
+ )
17
+ from data_engine.application.workspace import WorkspaceBinding, WorkspaceSessionApplication
18
+
19
+ __all__ = [
20
+ "ActionStateApplication",
21
+ "DaemonCommandResult",
22
+ "DetailApplication",
23
+ "EngineRunCompletion",
24
+ "FlowStateRefreshPlan",
25
+ "FlowRefreshResult",
26
+ "FlowCatalogApplication",
27
+ "FlowCatalogLoadResult",
28
+ "FlowCatalogPresentation",
29
+ "ManualRunCompletion",
30
+ "OperatorActionResult",
31
+ "OperatorControlApplication",
32
+ "RuntimeApplication",
33
+ "RuntimeLogMessage",
34
+ "RuntimeSnapshotPresentation",
35
+ "RuntimeSyncState",
36
+ "SelectedFlowPresentation",
37
+ "WorkspaceSessionApplication",
38
+ "WorkspaceBinding",
39
+ ]
@@ -0,0 +1,42 @@
1
+ """Host-agnostic operator action-state use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from data_engine.domain import OperatorActionContext, SelectedFlowState
6
+
7
+
8
+ class ActionStateApplication:
9
+ """Own host-neutral action-context assembly for operator surfaces."""
10
+
11
+ def build_action_context(
12
+ self,
13
+ *,
14
+ card,
15
+ flow_states: dict[str, str],
16
+ runtime_session,
17
+ flow_groups_by_name: dict[str, str | None],
18
+ active_flow_states,
19
+ has_logs: bool,
20
+ has_automated_flows: bool,
21
+ workspace_available: bool = True,
22
+ selected_run_group_present: bool = False,
23
+ ) -> OperatorActionContext:
24
+ """Return one operator action context from current runtime and selection state."""
25
+ selected_flow = SelectedFlowState.from_runtime(
26
+ card=card,
27
+ flow_states=flow_states,
28
+ runtime_session=runtime_session,
29
+ flow_groups_by_name=flow_groups_by_name,
30
+ active_flow_states=active_flow_states,
31
+ has_logs=has_logs,
32
+ )
33
+ return OperatorActionContext(
34
+ runtime_session=runtime_session,
35
+ selected_flow=selected_flow,
36
+ has_automated_flows=has_automated_flows,
37
+ workspace_available=workspace_available,
38
+ selected_run_group_present=selected_run_group_present,
39
+ )
40
+
41
+
42
+ __all__ = ["ActionStateApplication"]
@@ -0,0 +1,151 @@
1
+ """Host-agnostic flow catalog use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from data_engine.authoring.model import FlowValidationError
9
+ from data_engine.domain import FlowCatalogEntry, FlowCatalogLike, FlowCatalogState
10
+ from data_engine.platform.workspace_models import WorkspacePaths
11
+ from data_engine.services import FlowCatalogService
12
+ from data_engine.views.presentation import group_cards
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FlowCatalogLoadResult:
17
+ """Normalized result of one workspace catalog load attempt."""
18
+
19
+ catalog_state: FlowCatalogState
20
+ loaded: bool
21
+ error_text: str | None = None
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class FlowCatalogPresentation:
26
+ """Normalized grouped catalog presentation shared by operator surfaces."""
27
+
28
+ entries: tuple[FlowCatalogEntry, ...]
29
+ grouped_entries: tuple[tuple[str, tuple[FlowCatalogEntry, ...]], ...]
30
+ selected_flow_name: str | None
31
+
32
+ @property
33
+ def entries_by_name(self) -> dict[str, FlowCatalogEntry]:
34
+ """Return entries keyed by internal flow name."""
35
+ return {entry.name: entry for entry in self.entries}
36
+
37
+ @property
38
+ def selected_entry(self) -> FlowCatalogEntry | None:
39
+ """Return the normalized selected entry, if any."""
40
+ if self.selected_flow_name is None:
41
+ return None
42
+ return self.entries_by_name.get(self.selected_flow_name)
43
+
44
+ @property
45
+ def cards(self) -> tuple[FlowCatalogLike, ...]:
46
+ """Return catalog entries under the shared flow metadata protocol."""
47
+ return self.entries
48
+
49
+ @property
50
+ def grouped_cards(self) -> tuple[tuple[str, tuple[FlowCatalogLike, ...]], ...]:
51
+ """Return grouped entries under the shared flow metadata protocol."""
52
+ return self.grouped_entries
53
+
54
+ @property
55
+ def selected_card(self) -> FlowCatalogLike | None:
56
+ """Return the selected flow metadata under the shared flow protocol."""
57
+ return self.selected_entry
58
+
59
+ @property
60
+ def selected_list_index(self) -> int | None:
61
+ """Return the list index for the selected flow in a grouped header+item list."""
62
+ if self.selected_flow_name is None:
63
+ return None
64
+ index = 0
65
+ for _group_name, entries in self.grouped_entries:
66
+ index += 1
67
+ for entry in entries:
68
+ if entry.name == self.selected_flow_name:
69
+ return index
70
+ index += 1
71
+ return None
72
+
73
+
74
+ class FlowCatalogApplication:
75
+ """Own host-neutral flow catalog loading and state transitions."""
76
+
77
+ def __init__(self, *, flow_catalog_service: FlowCatalogService) -> None:
78
+ self.flow_catalog_service = flow_catalog_service
79
+
80
+ def load_state(
81
+ self,
82
+ *,
83
+ workspace_root: Path,
84
+ current_state: FlowCatalogState | None = None,
85
+ ) -> FlowCatalogState:
86
+ """Load discovered entries and merge them into one catalog state."""
87
+ base = current_state or FlowCatalogState.empty()
88
+ entries = self.flow_catalog_service.load_entries(workspace_root=workspace_root)
89
+ return base.with_entries(entries).with_empty_message("")
90
+
91
+ def empty_state(
92
+ self,
93
+ *,
94
+ message: str = "",
95
+ current_state: FlowCatalogState | None = None,
96
+ ) -> FlowCatalogState:
97
+ """Return an empty catalog state with one host-provided message."""
98
+ base = current_state or FlowCatalogState.empty()
99
+ return FlowCatalogState.empty(empty_message=message).with_selected_flow_name(base.selected_flow_name)
100
+
101
+ def select_flow(
102
+ self,
103
+ *,
104
+ catalog_state: FlowCatalogState,
105
+ flow_name: str | None,
106
+ ) -> FlowCatalogState:
107
+ """Return catalog state with one normalized selected flow."""
108
+ return catalog_state.with_selected_flow_name(flow_name)
109
+
110
+ def build_presentation(
111
+ self,
112
+ *,
113
+ catalog_state: FlowCatalogState,
114
+ ) -> FlowCatalogPresentation:
115
+ """Return grouped UI-friendly catalog presentation from one catalog state."""
116
+ grouped = tuple(
117
+ (bucket.group_name, bucket.entries)
118
+ for bucket in group_cards(catalog_state.entries)
119
+ )
120
+ return FlowCatalogPresentation(
121
+ entries=catalog_state.entries,
122
+ grouped_entries=grouped,
123
+ selected_flow_name=catalog_state.selected_flow_name,
124
+ )
125
+
126
+ def load_workspace_catalog(
127
+ self,
128
+ *,
129
+ workspace_paths: WorkspacePaths,
130
+ current_state: FlowCatalogState | None = None,
131
+ missing_message: str = "No flow modules discovered.",
132
+ ) -> FlowCatalogLoadResult:
133
+ """Return one normalized catalog load result for a resolved workspace binding."""
134
+ if not workspace_paths.flow_modules_dir.is_dir():
135
+ return FlowCatalogLoadResult(
136
+ catalog_state=self.empty_state(message=missing_message, current_state=current_state),
137
+ loaded=False,
138
+ )
139
+ try:
140
+ catalog_state = self.load_state(
141
+ workspace_root=workspace_paths.workspace_root,
142
+ current_state=current_state,
143
+ )
144
+ except FlowValidationError as exc:
145
+ message = str(exc)
146
+ return FlowCatalogLoadResult(
147
+ catalog_state=self.empty_state(message=message, current_state=current_state),
148
+ loaded=False,
149
+ error_text=message,
150
+ )
151
+ return FlowCatalogLoadResult(catalog_state=catalog_state, loaded=True)
@@ -0,0 +1,213 @@
1
+ """Host-agnostic operator control and action use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from data_engine.domain import RuntimeSessionState
8
+ from data_engine.hosts.daemon.manager import WorkspaceDaemonManager
9
+ from data_engine.platform.workspace_models import WorkspacePaths, authored_workspace_is_available
10
+ from data_engine.services import DaemonStateService
11
+
12
+ from data_engine.application.runtime import RuntimeApplication
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class OperatorActionResult:
17
+ """Normalized result for one operator control action."""
18
+
19
+ requested: bool
20
+ sync_after: bool = False
21
+ ensure_daemon_started: bool = False
22
+ status_text: str | None = None
23
+ error_text: str | None = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FlowRefreshResult:
28
+ """Normalized result for one flow-refresh request."""
29
+
30
+ reload_catalog: bool
31
+ sync_after: bool = False
32
+ status_text: str | None = None
33
+ warning_text: str | None = None
34
+ error_text: str | None = None
35
+
36
+
37
+ class OperatorControlApplication:
38
+ """Own host-neutral action orchestration for operator surfaces."""
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ runtime_application: RuntimeApplication,
44
+ daemon_state_service: DaemonStateService,
45
+ ) -> None:
46
+ self.runtime_application = runtime_application
47
+ self.daemon_state_service = daemon_state_service
48
+
49
+ def run_selected_flow(
50
+ self,
51
+ *,
52
+ paths: WorkspacePaths,
53
+ runtime_session: RuntimeSessionState,
54
+ selected_flow_name: str | None,
55
+ selected_flow_valid: bool,
56
+ selected_flow_group: str | None,
57
+ selected_flow_group_active: bool,
58
+ blocked_status_text: str,
59
+ timeout: float = 2.0,
60
+ ) -> OperatorActionResult:
61
+ """Validate and request one manual run for the selected flow."""
62
+ if not authored_workspace_is_available(paths):
63
+ return OperatorActionResult(requested=False, error_text="Workspace root is no longer available.")
64
+ if selected_flow_name is None:
65
+ return OperatorActionResult(requested=False, status_text="Select one flow first.")
66
+ if not selected_flow_valid:
67
+ return OperatorActionResult(
68
+ requested=False,
69
+ status_text=f"{selected_flow_name} is invalid and cannot run.",
70
+ )
71
+ if selected_flow_group_active or runtime_session.runtime_active or runtime_session.manual_run_active:
72
+ return OperatorActionResult(requested=False)
73
+ if not runtime_session.control_available:
74
+ return OperatorActionResult(requested=False, status_text=blocked_status_text)
75
+ result = self.runtime_application.run_flow(
76
+ paths,
77
+ name=selected_flow_name,
78
+ wait=False,
79
+ timeout=timeout,
80
+ )
81
+ if not result.ok:
82
+ return OperatorActionResult(
83
+ requested=False,
84
+ error_text=_verbose_action_error(
85
+ f"run {selected_flow_name}",
86
+ result.error,
87
+ ),
88
+ )
89
+ return OperatorActionResult(
90
+ requested=True,
91
+ sync_after=True,
92
+ status_text=f"Running {selected_flow_name}...",
93
+ )
94
+
95
+ def start_engine(
96
+ self,
97
+ *,
98
+ paths: WorkspacePaths,
99
+ runtime_session: RuntimeSessionState,
100
+ has_automated_flows: bool,
101
+ blocked_status_text: str,
102
+ timeout: float = 2.0,
103
+ ) -> OperatorActionResult:
104
+ """Validate and request automated engine start."""
105
+ if not authored_workspace_is_available(paths):
106
+ return OperatorActionResult(requested=False, error_text="Workspace root is no longer available.")
107
+ if runtime_session.runtime_active or runtime_session.runtime_stopping or runtime_session.manual_run_active:
108
+ return OperatorActionResult(requested=False)
109
+ if not runtime_session.control_available:
110
+ return OperatorActionResult(requested=False, status_text=blocked_status_text)
111
+ if not has_automated_flows:
112
+ return OperatorActionResult(requested=False, status_text="No automated flows are available.")
113
+ result = self.runtime_application.start_engine(paths, timeout=timeout)
114
+ if not result.ok:
115
+ return OperatorActionResult(
116
+ requested=False,
117
+ error_text=_verbose_action_error("start the automated engine", result.error),
118
+ )
119
+ return OperatorActionResult(requested=True, sync_after=True, status_text="Starting automated engine...")
120
+
121
+ def stop_pipeline(
122
+ self,
123
+ *,
124
+ paths: WorkspacePaths,
125
+ runtime_session: RuntimeSessionState,
126
+ selected_flow_group: str | None,
127
+ blocked_status_text: str,
128
+ timeout: float = 2.0,
129
+ ) -> OperatorActionResult:
130
+ """Validate and request stop for the engine or selected manual flow."""
131
+ if runtime_session.runtime_active:
132
+ result = self.runtime_application.stop_engine(paths, timeout=timeout)
133
+ if not result.ok:
134
+ return OperatorActionResult(
135
+ requested=False,
136
+ error_text=_verbose_action_error("stop the engine", result.error),
137
+ )
138
+ return OperatorActionResult(requested=True, sync_after=True, status_text="Stopping engine...")
139
+ if runtime_session.manual_run_active:
140
+ if not runtime_session.control_available:
141
+ return OperatorActionResult(requested=False, status_text=blocked_status_text)
142
+ flow_name = runtime_session.active_manual_runs.get(selected_flow_group)
143
+ if flow_name is None:
144
+ return OperatorActionResult(requested=False)
145
+ result = self.runtime_application.stop_flow(paths, name=flow_name, timeout=timeout)
146
+ if not result.ok:
147
+ return OperatorActionResult(
148
+ requested=False,
149
+ error_text=_verbose_action_error(f"stop {flow_name}", result.error),
150
+ )
151
+ return OperatorActionResult(requested=True, sync_after=True, status_text="Stopping selected flow...")
152
+ return OperatorActionResult(requested=False)
153
+
154
+ def request_control(self, daemon_manager: WorkspaceDaemonManager) -> OperatorActionResult:
155
+ """Request workspace control through the daemon-state manager."""
156
+ try:
157
+ message = self.daemon_state_service.request_control(daemon_manager)
158
+ except Exception as exc:
159
+ return OperatorActionResult(
160
+ requested=False,
161
+ error_text=_verbose_action_error("request workspace control", exc),
162
+ )
163
+ return OperatorActionResult(
164
+ requested=True,
165
+ sync_after=True,
166
+ ensure_daemon_started=True,
167
+ status_text=message,
168
+ )
169
+
170
+ def refresh_flows(
171
+ self,
172
+ *,
173
+ paths: WorkspacePaths,
174
+ runtime_session: RuntimeSessionState,
175
+ has_authored_workspace: bool,
176
+ timeout: float = 5.0,
177
+ ) -> FlowRefreshResult:
178
+ """Validate and request one flow refresh while preserving local reload behavior."""
179
+ if runtime_session.runtime_active or runtime_session.active_manual_runs:
180
+ return FlowRefreshResult(
181
+ reload_catalog=False,
182
+ error_text="Stop active engine or manual runs before refreshing flows.",
183
+ )
184
+ if not has_authored_workspace:
185
+ return FlowRefreshResult(
186
+ reload_catalog=True,
187
+ sync_after=True,
188
+ status_text="No flow modules discovered.",
189
+ )
190
+ result = self.runtime_application.refresh_flows(paths, timeout=timeout)
191
+ if not result.ok:
192
+ return FlowRefreshResult(
193
+ reload_catalog=True,
194
+ sync_after=True,
195
+ status_text="Reloaded flow definitions.",
196
+ warning_text=_verbose_action_error("refresh flows", result.error),
197
+ )
198
+ return FlowRefreshResult(
199
+ reload_catalog=True,
200
+ sync_after=True,
201
+ status_text="Reloaded flow definitions.",
202
+ )
203
+
204
+
205
+ def _verbose_action_error(action: str, detail: object | None) -> str:
206
+ """Return a non-terse user-facing failure string for operator control paths."""
207
+ text = str(detail).strip() if detail is not None else ""
208
+ if text:
209
+ return text
210
+ return f"Failed to {action}. The daemon returned no additional detail."
211
+
212
+
213
+ __all__ = ["FlowRefreshResult", "OperatorActionResult", "OperatorControlApplication"]
@@ -0,0 +1,73 @@
1
+ """Host-agnostic selected-flow and run-detail use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from data_engine.domain import FlowCatalogLike, FlowRunState, SelectedFlowDetailState
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class SelectedFlowPresentation:
12
+ """Normalized selected-flow detail state for operator surfaces."""
13
+
14
+ detail_state: SelectedFlowDetailState | None
15
+ run_groups: tuple[FlowRunState, ...]
16
+ visible_run_groups: tuple[FlowRunState, ...]
17
+ selected_run_key: tuple[str, str] | None
18
+ empty_text: str
19
+
20
+ @property
21
+ def run_group_signature(self) -> tuple[tuple[str, str], ...]:
22
+ """Return the stable visible run-list signature for diffing/render reuse."""
23
+ return tuple(group.key for group in self.visible_run_groups)
24
+
25
+ @property
26
+ def selected_run_group(self) -> FlowRunState | None:
27
+ """Return the normalized selected run group, if any."""
28
+ if self.selected_run_key is not None:
29
+ for run_group in self.run_groups:
30
+ if run_group.key == self.selected_run_key:
31
+ return run_group
32
+ return self.run_groups[0] if self.run_groups else None
33
+
34
+
35
+ class DetailApplication:
36
+ """Own host-neutral selected-flow detail and run selection behavior."""
37
+
38
+ def build_selected_flow_presentation(
39
+ self,
40
+ *,
41
+ card: FlowCatalogLike | None,
42
+ tracker,
43
+ flow_states: dict[str, str],
44
+ run_groups: tuple[FlowRunState, ...],
45
+ selected_run_key: tuple[str, str] | None,
46
+ max_visible_runs: int | None = None,
47
+ ) -> SelectedFlowPresentation:
48
+ """Return the selected-flow detail state and normalized run selection."""
49
+ if card is None:
50
+ return SelectedFlowPresentation(
51
+ detail_state=None,
52
+ run_groups=(),
53
+ visible_run_groups=(),
54
+ selected_run_key=None,
55
+ empty_text="Select one flow to see details.",
56
+ )
57
+ detail_state = SelectedFlowDetailState.from_flow(
58
+ card,
59
+ tracker,
60
+ flow_states=flow_states,
61
+ )
62
+ normalized_key = selected_run_key if any(group.key == selected_run_key for group in run_groups) else (run_groups[0].key if run_groups else None)
63
+ visible_run_groups = run_groups[-max_visible_runs:] if max_visible_runs is not None and max_visible_runs >= 0 else run_groups
64
+ return SelectedFlowPresentation(
65
+ detail_state=detail_state,
66
+ run_groups=run_groups,
67
+ visible_run_groups=visible_run_groups,
68
+ selected_run_key=normalized_key,
69
+ empty_text="",
70
+ )
71
+
72
+
73
+ __all__ = ["DetailApplication", "SelectedFlowPresentation"]