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,439 @@
1
+ """Flow loading, selection, and action-state controllers for the desktop GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QTimer
8
+
9
+ from data_engine.application import FlowCatalogApplication, WorkspaceSessionApplication
10
+ from data_engine.services import LogService
11
+ from data_engine.platform.identity import APP_DISPLAY_NAME
12
+ from data_engine.views import GuiActionState, QtFlowCard, surface_control_status_text
13
+
14
+ if TYPE_CHECKING:
15
+ from data_engine.ui.gui.app import DataEngineWindow
16
+
17
+
18
+ class _GuiWorkspaceCatalogController:
19
+ """Own GUI workspace binding and catalog refresh orchestration."""
20
+
21
+ def __init__(
22
+ self,
23
+ *,
24
+ workspace_session_application: WorkspaceSessionApplication,
25
+ flow_catalog_application: FlowCatalogApplication,
26
+ ) -> None:
27
+ self.workspace_session_application = workspace_session_application
28
+ self.flow_catalog_application = flow_catalog_application
29
+
30
+ def load_flows(self, window: "DataEngineWindow", presentation: "_GuiFlowPresentationController") -> None:
31
+ missing_message = (
32
+ "Workspace collection root is not configured."
33
+ if not window.workspace_paths.workspace_configured
34
+ else "No flow modules discovered."
35
+ )
36
+ result = self.flow_catalog_application.load_workspace_catalog(
37
+ workspace_paths=window.workspace_paths,
38
+ current_state=window.flow_catalog_state,
39
+ missing_message=missing_message,
40
+ )
41
+ if not result.loaded and result.error_text is None:
42
+ window.flow_catalog_state = result.catalog_state.with_empty_message(
43
+ window._empty_flow_message_for_error(missing_message)
44
+ ).with_selected_flow_name(None)
45
+ self.populate_flow_tree(window)
46
+ presentation.refresh_selection(window, None)
47
+ window._refresh_log_view(force_scroll_to_bottom=True)
48
+ presentation.refresh_action_buttons(window)
49
+ presentation.refresh_summary(window)
50
+ window._refresh_workspace_visibility_panel()
51
+ return
52
+ if result.error_text is not None:
53
+ message = result.error_text
54
+ window.flow_catalog_state = result.catalog_state.with_empty_message(
55
+ window._empty_flow_message_for_error(message)
56
+ ).with_selected_flow_name(None)
57
+ self.populate_flow_tree(window)
58
+ window._append_log_line(f"Failed to load flows: {message}")
59
+ presentation.refresh_selection(window, None)
60
+ window._refresh_log_view(force_scroll_to_bottom=True)
61
+ presentation.refresh_action_buttons(window)
62
+ presentation.refresh_summary(window)
63
+ window._refresh_workspace_visibility_panel()
64
+ if not window._is_bootstrap_ready_error(message):
65
+ window._show_message_box(
66
+ title=APP_DISPLAY_NAME,
67
+ text=f"Failed to load flows.\n\n{message}",
68
+ tone="error",
69
+ )
70
+ return
71
+
72
+ window.flow_catalog_state = result.catalog_state
73
+ self.populate_flow_tree(window)
74
+ presentation.select_flow(window, window.selected_flow_name)
75
+ presentation.refresh_summary(window)
76
+ window._refresh_workspace_visibility_panel()
77
+ window._rebuild_runtime_snapshot()
78
+
79
+ def populate_flow_tree(self, window: "DataEngineWindow") -> None:
80
+ presentation = self.flow_catalog_application.build_presentation(
81
+ catalog_state=window.flow_catalog_state,
82
+ )
83
+ window.sidebar_flow_widgets = {}
84
+ window.sidebar_group_widgets = {}
85
+ window.sidebar_content.setUpdatesEnabled(False)
86
+ while window.sidebar_layout.count() > 1:
87
+ item = window.sidebar_layout.takeAt(0)
88
+ widget = item.widget()
89
+ if widget is not None:
90
+ widget.setParent(None)
91
+ widget.deleteLater()
92
+ for group_name, entries in presentation.grouped_cards:
93
+ group_widget = window._build_group_row_widget(group_name, list(entries))
94
+ window.sidebar_group_widgets[group_name] = group_widget
95
+ window.sidebar_layout.insertWidget(window.sidebar_layout.count() - 1, group_widget)
96
+ for index, card in enumerate(entries, start=1):
97
+ widget = window._build_flow_row_widget(card)
98
+ widget.setProperty("flowIndex", index)
99
+ window.sidebar_flow_widgets[card.name] = widget
100
+ window.sidebar_layout.insertWidget(window.sidebar_layout.count() - 1, widget)
101
+ window.sidebar_content.setUpdatesEnabled(True)
102
+ window.sidebar_content.updateGeometry()
103
+ window.sidebar_content.update()
104
+ window._refresh_sidebar_selection()
105
+ window._update_sidebar_scroll_cues()
106
+
107
+ def reload_workspace_options(self, window: "DataEngineWindow") -> None:
108
+ window.workspace_session_state = self.workspace_session_application.refresh_session(
109
+ workspace_paths=window.workspace_paths,
110
+ override_root=window.workspace_collection_root_override,
111
+ )
112
+ current_id = window.workspace_session_state.current_workspace_id
113
+ workspace_ids = window.workspace_session_state.discovered_workspace_ids
114
+ window.workspace_selector.blockSignals(True)
115
+ try:
116
+ window.workspace_selector.clear()
117
+ if not workspace_ids:
118
+ window.workspace_selector.addItem("(no workspace)", "")
119
+ window.workspace_selector.setCurrentIndex(0)
120
+ window.workspace_selector.setEnabled(False)
121
+ else:
122
+ for workspace_id in workspace_ids:
123
+ window.workspace_selector.addItem(workspace_id, workspace_id)
124
+ selected_index = window.workspace_selector.findData(current_id)
125
+ if selected_index < 0:
126
+ selected_index = 0
127
+ window.workspace_selector.setCurrentIndex(selected_index)
128
+ window.workspace_selector.setEnabled(True)
129
+ finally:
130
+ window.workspace_selector.blockSignals(False)
131
+
132
+ def workspace_selection_changed(self, window: "DataEngineWindow", index: int) -> None:
133
+ if index < 0:
134
+ return
135
+ workspace_id = str(window.workspace_selector.itemData(index) or "").strip()
136
+ if not workspace_id or workspace_id == window.workspace_paths.workspace_id:
137
+ return
138
+ self.switch_workspace(window, workspace_id)
139
+
140
+ def switch_workspace(self, window: "DataEngineWindow", workspace_id: str) -> None:
141
+ try:
142
+ window.workspace_selector.hidePopup()
143
+ except Exception:
144
+ pass
145
+ window._workspace_switch_generation += 1
146
+ switch_generation = window._workspace_switch_generation
147
+
148
+ def _finish_switch() -> None:
149
+ if switch_generation != window._workspace_switch_generation:
150
+ return
151
+ if window.ui_closing:
152
+ return
153
+ if workspace_id == window.workspace_paths.workspace_id:
154
+ return
155
+ window._rebind_workspace_context(workspace_id=workspace_id)
156
+
157
+ QTimer.singleShot(0, _finish_switch)
158
+
159
+ def refresh_flows_requested(self, window: "DataEngineWindow", presentation: "_GuiFlowPresentationController") -> None:
160
+ result = window.control_application.refresh_flows(
161
+ paths=window.workspace_paths,
162
+ runtime_session=window.runtime_session,
163
+ has_authored_workspace=window._has_authored_workspace(),
164
+ timeout=5.0,
165
+ )
166
+ if result.error_text is not None:
167
+ window._show_message_box_later(
168
+ title=APP_DISPLAY_NAME,
169
+ text=result.error_text,
170
+ tone="error",
171
+ )
172
+ return
173
+ if result.reload_catalog:
174
+ self.reload_workspace_options(window)
175
+ self.load_flows(window, presentation)
176
+ if result.sync_after:
177
+ window._sync_from_daemon()
178
+ if result.status_text is not None and window.flow_cards:
179
+ window._append_log_line(result.status_text)
180
+ if result.warning_text is not None:
181
+ window._append_log_line(f"Flow refresh warning: {result.warning_text}")
182
+ if window.flow_cards:
183
+ window._show_message_box_later(
184
+ title=APP_DISPLAY_NAME,
185
+ text=f"Refreshed local flow definitions, but daemon refresh failed.\n\n{result.warning_text}",
186
+ tone="error",
187
+ )
188
+
189
+
190
+ class _GuiFlowPresentationController:
191
+ """Own GUI selection, action-state, and summary presentation orchestration."""
192
+
193
+ def __init__(
194
+ self,
195
+ *,
196
+ flow_catalog_application: FlowCatalogApplication,
197
+ log_service: LogService,
198
+ ) -> None:
199
+ self.flow_catalog_application = flow_catalog_application
200
+ self.log_service = log_service
201
+
202
+ def select_flow(self, window: "DataEngineWindow", flow_name: str | None) -> None:
203
+ window.flow_catalog_state = self.flow_catalog_application.select_flow(
204
+ catalog_state=window.flow_catalog_state,
205
+ flow_name=flow_name,
206
+ )
207
+ presentation = self.flow_catalog_application.build_presentation(
208
+ catalog_state=window.flow_catalog_state,
209
+ )
210
+ if presentation.selected_card is None:
211
+ self.refresh_selection(window, None)
212
+ self.refresh_action_buttons(window)
213
+ window._refresh_sidebar_selection()
214
+ window._refresh_log_view(force_scroll_to_bottom=True)
215
+ return
216
+ self.refresh_selection(window, presentation.selected_card)
217
+ self.refresh_action_buttons(window)
218
+ window._refresh_sidebar_selection()
219
+ window._refresh_log_view(force_scroll_to_bottom=True)
220
+
221
+ def refresh_selection(self, window: "DataEngineWindow", card: QtFlowCard | None) -> None:
222
+ presentation = window.detail_application.build_selected_flow_presentation(
223
+ card=card,
224
+ tracker=window.operation_tracker,
225
+ flow_states=window.flow_states,
226
+ run_groups=(),
227
+ selected_run_key=None,
228
+ )
229
+ if presentation.detail_state is None:
230
+ window.flow_error_label.clear()
231
+ window._set_operation_cards(())
232
+ return
233
+
234
+ window.flow_error_label.setText(presentation.detail_state.error)
235
+ window._set_operation_cards(tuple(row.name for row in presentation.detail_state.operation_rows))
236
+ assert card is not None
237
+ window._render_operation_durations(card.name)
238
+
239
+ def refresh_summary(self, window: "DataEngineWindow") -> None:
240
+ self.refresh_lease_status(window)
241
+
242
+ def refresh_action_buttons(self, window: "DataEngineWindow") -> None:
243
+ card = window.flow_cards.get(window.selected_flow_name or "")
244
+ action_context = window.action_state_application.build_action_context(
245
+ card=card,
246
+ flow_states=window.flow_states,
247
+ runtime_session=window.runtime_session,
248
+ flow_groups_by_name={flow_name: flow_card.group for flow_name, flow_card in window.flow_cards.items()},
249
+ active_flow_states=window._ACTIVE_FLOW_STATES,
250
+ has_logs=bool(
251
+ card is not None and self.log_service.entries_for_flow(window.runtime_binding.log_store, card.name)
252
+ ),
253
+ has_automated_flows=any(flow_card.valid and flow_card.mode in {"poll", "schedule"} for flow_card in window.flow_cards.values()),
254
+ workspace_available=window._has_authored_workspace(),
255
+ )
256
+ action_state = GuiActionState.from_context(action_context)
257
+ window.flow_run_button.setText(action_state.flow_run_label)
258
+ window.flow_run_button.setEnabled(action_state.flow_run_enabled)
259
+ window.flow_config_button.setEnabled(action_state.flow_config_enabled)
260
+ window.engine_button.setEnabled(action_state.engine_enabled)
261
+ window.engine_button.setText(action_state.engine_label)
262
+ window.engine_button.setProperty("engineState", action_state.engine_state)
263
+ window.refresh_button.setEnabled(action_state.refresh_enabled)
264
+ window.clear_flow_log_button.setEnabled(action_state.clear_flow_log_enabled)
265
+ window.request_control_button.setVisible(action_state.request_control_visible)
266
+ window.request_control_button.setEnabled(action_state.request_control_enabled)
267
+ if window.workspace_selector.count() > 0:
268
+ window.workspace_selector.setEnabled(bool(window.workspace_session_state.discovered_workspace_ids))
269
+ style = window.engine_button.style()
270
+ style.unpolish(window.engine_button)
271
+ style.polish(window.engine_button)
272
+ window.engine_button.update()
273
+
274
+ def refresh_lease_status(self, window: "DataEngineWindow") -> None:
275
+ snapshot = window.daemon_state_service.sync(window.runtime_binding.daemon_manager)
276
+ window.workspace_control_state = window.daemon_state_service.control_state(
277
+ window.runtime_binding.daemon_manager,
278
+ snapshot,
279
+ daemon_startup_in_progress=window._daemon_startup_in_progress,
280
+ )
281
+ status_text = surface_control_status_text(
282
+ window.workspace_control_state.control_status_text,
283
+ empty_flow_message=window.empty_flow_message,
284
+ )
285
+ if not status_text:
286
+ window.lease_status_label.clear()
287
+ window.lease_status_label.setVisible(False)
288
+ return
289
+ window.lease_status_label.setText(status_text)
290
+ window.lease_status_label.setVisible(True)
291
+
292
+ def request_control(self, window: "DataEngineWindow") -> None:
293
+ result = window.control_application.request_control(window.runtime_binding.daemon_manager)
294
+ if result.error_text is not None:
295
+ window._append_log_line(result.error_text.replace("\n\n", ": "))
296
+ window._show_message_box(
297
+ title=APP_DISPLAY_NAME,
298
+ text=result.error_text,
299
+ tone="error",
300
+ )
301
+ return
302
+ if result.status_text is not None:
303
+ window._append_log_line(result.status_text)
304
+ if result.ensure_daemon_started:
305
+ window._ensure_daemon_started()
306
+ if result.sync_after:
307
+ window._sync_from_daemon()
308
+
309
+ def update_engine_button(self, window: "DataEngineWindow") -> None:
310
+ self.refresh_action_buttons(window)
311
+
312
+ def set_flow_state(self, window: "DataEngineWindow", flow_name: str, state: str) -> None:
313
+ self.set_flow_states(window, {flow_name: state})
314
+
315
+ def set_flow_states(self, window: "DataEngineWindow", updates: dict[str, str]) -> None:
316
+ if not updates:
317
+ return
318
+ next_states = dict(window.flow_states)
319
+ next_states.update(updates)
320
+ refresh_plan = window.runtime_application.plan_flow_state_refresh(
321
+ previous_states=window.flow_states,
322
+ next_states=next_states,
323
+ runtime_session=window.runtime_session,
324
+ )
325
+ if not refresh_plan.changed_flow_names:
326
+ return
327
+ window.flow_states = refresh_plan.flow_states
328
+ window._refresh_sidebar_state_views(set(refresh_plan.changed_flow_names))
329
+ if window.selected_flow_name is not None and window.selected_flow_name in refresh_plan.changed_flow_names:
330
+ self.refresh_selection(window, window.flow_cards[window.selected_flow_name])
331
+ self.refresh_summary(window)
332
+
333
+ def refresh_flows_requested(self, window: "DataEngineWindow") -> None:
334
+ result = window.control_application.refresh_flows(
335
+ paths=window.workspace_paths,
336
+ runtime_session=window.runtime_session,
337
+ has_authored_workspace=window._has_authored_workspace(),
338
+ timeout=5.0,
339
+ )
340
+ if result.error_text is not None:
341
+ window._show_message_box_later(
342
+ title=APP_DISPLAY_NAME,
343
+ text=result.error_text,
344
+ tone="error",
345
+ )
346
+ return
347
+ if result.reload_catalog:
348
+ self.reload_workspace_options(window)
349
+ self.load_flows(window)
350
+ if result.sync_after:
351
+ window._sync_from_daemon()
352
+ if result.status_text is not None and window.flow_cards:
353
+ window._append_log_line(result.status_text)
354
+ if result.warning_text is not None:
355
+ window._append_log_line(f"Flow refresh warning: {result.warning_text}")
356
+ if window.flow_cards:
357
+ window._show_message_box_later(
358
+ title=APP_DISPLAY_NAME,
359
+ text=f"Refreshed local flow definitions, but daemon refresh failed.\n\n{result.warning_text}",
360
+ tone="error",
361
+ )
362
+
363
+ def clear_logs(self, window: "DataEngineWindow") -> None:
364
+ if window.selected_flow_name is None:
365
+ return
366
+ self.log_service.clear_flow(window.runtime_binding.log_store, window.selected_flow_name)
367
+ window._refresh_log_view(force_scroll_to_bottom=True)
368
+ self.refresh_action_buttons(window)
369
+
370
+
371
+ class GuiFlowController:
372
+ """Compose narrower GUI flow collaborators behind one stable controller seam."""
373
+
374
+ def __init__(
375
+ self,
376
+ *,
377
+ workspace_session_application: WorkspaceSessionApplication,
378
+ flow_catalog_application: FlowCatalogApplication,
379
+ log_service: LogService,
380
+ ) -> None:
381
+ self.workspace = _GuiWorkspaceCatalogController(
382
+ workspace_session_application=workspace_session_application,
383
+ flow_catalog_application=flow_catalog_application,
384
+ )
385
+ self.presentation = _GuiFlowPresentationController(
386
+ flow_catalog_application=flow_catalog_application,
387
+ log_service=log_service,
388
+ )
389
+
390
+ def load_flows(self, window: "DataEngineWindow") -> None:
391
+ self.workspace.load_flows(window, self.presentation)
392
+
393
+ def populate_flow_tree(self, window: "DataEngineWindow") -> None:
394
+ self.workspace.populate_flow_tree(window)
395
+
396
+ def select_flow(self, window: "DataEngineWindow", flow_name: str | None) -> None:
397
+ self.presentation.select_flow(window, flow_name)
398
+
399
+ def refresh_selection(self, window: "DataEngineWindow", card: QtFlowCard | None) -> None:
400
+ self.presentation.refresh_selection(window, card)
401
+
402
+ def refresh_summary(self, window: "DataEngineWindow") -> None:
403
+ self.presentation.refresh_summary(window)
404
+
405
+ def refresh_action_buttons(self, window: "DataEngineWindow") -> None:
406
+ self.presentation.refresh_action_buttons(window)
407
+
408
+ def reload_workspace_options(self, window: "DataEngineWindow") -> None:
409
+ self.workspace.reload_workspace_options(window)
410
+
411
+ def workspace_selection_changed(self, window: "DataEngineWindow", index: int) -> None:
412
+ self.workspace.workspace_selection_changed(window, index)
413
+
414
+ def switch_workspace(self, window: "DataEngineWindow", workspace_id: str) -> None:
415
+ self.workspace.switch_workspace(window, workspace_id)
416
+
417
+ def refresh_lease_status(self, window: "DataEngineWindow") -> None:
418
+ self.presentation.refresh_lease_status(window)
419
+
420
+ def request_control(self, window: "DataEngineWindow") -> None:
421
+ self.presentation.request_control(window)
422
+
423
+ def update_engine_button(self, window: "DataEngineWindow") -> None:
424
+ self.presentation.update_engine_button(window)
425
+
426
+ def set_flow_state(self, window: "DataEngineWindow", flow_name: str, state: str) -> None:
427
+ self.presentation.set_flow_state(window, flow_name, state)
428
+
429
+ def set_flow_states(self, window: "DataEngineWindow", updates: dict[str, str]) -> None:
430
+ self.presentation.set_flow_states(window, updates)
431
+
432
+ def refresh_flows_requested(self, window: "DataEngineWindow") -> None:
433
+ self.workspace.refresh_flows_requested(window, self.presentation)
434
+
435
+ def clear_logs(self, window: "DataEngineWindow") -> None:
436
+ self.presentation.clear_logs(window)
437
+
438
+
439
+ __all__ = ["GuiFlowController"]
@@ -0,0 +1,245 @@
1
+ """Runtime and daemon orchestration controllers for the desktop GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from data_engine.application import RuntimeApplication
8
+ from data_engine.services import DaemonService, LogService
9
+ from data_engine.domain import DaemonStatusState, RuntimeSessionState, WorkspaceControlState
10
+ from data_engine.platform.identity import APP_DISPLAY_NAME
11
+ from data_engine.ui.gui.helpers import start_worker_thread
12
+ if TYPE_CHECKING:
13
+ from data_engine.ui.gui.app import DataEngineWindow
14
+
15
+
16
+ class GuiRuntimeController:
17
+ """Own daemon/runtime orchestration for the desktop GUI."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ runtime_application: RuntimeApplication,
23
+ daemon_service: DaemonService,
24
+ log_service: LogService,
25
+ ) -> None:
26
+ self.runtime_application = runtime_application
27
+ self.daemon_service = daemon_service
28
+ self.log_service = log_service
29
+
30
+ def sync_from_daemon(self, window: "DataEngineWindow") -> None:
31
+ if not window._has_authored_workspace():
32
+ window.daemon_status = DaemonStatusState.empty()
33
+ window.workspace_control_state = WorkspaceControlState.empty()
34
+ window.runtime_session = RuntimeSessionState.empty()
35
+ window.flow_controller.reload_workspace_options(window)
36
+ window.flow_controller.load_flows(window)
37
+ return
38
+ try:
39
+ live = self.daemon_service.is_live(window.workspace_paths)
40
+ except Exception:
41
+ live = False
42
+ if not live and window._auto_daemon_enabled:
43
+ self.ensure_daemon_started(window)
44
+ sync_state = self.runtime_application.sync_state(
45
+ paths=window.workspace_paths,
46
+ daemon_manager=window.runtime_binding.daemon_manager,
47
+ flow_cards=window.flow_cards.values(),
48
+ runtime_ledger=window.runtime_binding.runtime_ledger,
49
+ daemon_startup_in_progress=window._daemon_startup_in_progress,
50
+ )
51
+ window.daemon_status = sync_state.daemon_status
52
+ window.workspace_control_state = sync_state.workspace_control_state
53
+ window.runtime_session = sync_state.runtime_session
54
+ window._apply_daemon_snapshot(sync_state.snapshot)
55
+ self.rebuild_runtime_snapshot(window)
56
+
57
+ def ensure_daemon_started(self, window: "DataEngineWindow") -> bool:
58
+ if not window._has_authored_workspace():
59
+ return False
60
+ try:
61
+ if self.daemon_service.is_live(window.workspace_paths):
62
+ return True
63
+ except Exception:
64
+ pass
65
+ if window._daemon_startup_in_progress or not window._auto_daemon_enabled or window.ui_closing:
66
+ return False
67
+ now = window._monotonic()
68
+ if now - window._last_daemon_spawn_attempt < 2.0:
69
+ return False
70
+ window._last_daemon_spawn_attempt = now
71
+ window._daemon_startup_in_progress = True
72
+ start_worker_thread(window, target=window._start_daemon_worker)
73
+ return False
74
+
75
+ def start_daemon_worker(self, window: "DataEngineWindow") -> None:
76
+ success = False
77
+ error_text = ""
78
+ spawn_result = self.runtime_application.spawn_daemon(window.workspace_paths)
79
+ if not spawn_result.ok:
80
+ error_text = spawn_result.error
81
+ else:
82
+ success = self.daemon_service.is_live(window.workspace_paths)
83
+ if not success and not error_text:
84
+ error_text = "Daemon startup did not provide any additional error details."
85
+ window.signals.daemon_startup_finished.emit(success, error_text)
86
+
87
+ def rebuild_runtime_snapshot(self, window: "DataEngineWindow") -> None:
88
+ self.log_service.reload(window.runtime_binding.log_store)
89
+ window._rehydrate_step_outputs_from_ledger()
90
+ snapshot = self.runtime_application.build_runtime_snapshot(
91
+ flow_cards=window.flow_cards.values(),
92
+ log_entries=self.log_service.all_entries(window.runtime_binding.log_store),
93
+ runtime_session=window.runtime_session,
94
+ now=window._monotonic(),
95
+ )
96
+ refresh_plan = self.runtime_application.plan_flow_state_refresh(
97
+ previous_states=window.flow_states,
98
+ next_states=snapshot.flow_states,
99
+ runtime_session=window.runtime_session,
100
+ )
101
+ window.operation_tracker = snapshot.operation_tracker
102
+ window.flow_states = refresh_plan.flow_states
103
+ window._refresh_sidebar_state_views(set(refresh_plan.changed_flow_names))
104
+ if window.selected_flow_name is not None and window.selected_flow_name in window.flow_cards:
105
+ window.flow_controller.refresh_selection(window, window.flow_cards[window.selected_flow_name])
106
+ window.flow_controller.refresh_summary(window)
107
+ window._refresh_workspace_visibility_panel()
108
+ window._refresh_log_view()
109
+ window.flow_controller.refresh_action_buttons(window)
110
+
111
+ def start_runtime(self, window: "DataEngineWindow") -> None:
112
+ if not window._has_authored_workspace():
113
+ window._sync_from_daemon()
114
+ return
115
+ result = window.control_application.start_engine(
116
+ paths=window.workspace_paths,
117
+ runtime_session=window.runtime_session,
118
+ has_automated_flows=any(card.valid and card.mode in {"poll", "schedule"} for card in window.flow_cards.values()),
119
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
120
+ timeout=2.0,
121
+ )
122
+ if result.error_text is not None:
123
+ window._show_message_box_later(
124
+ title=APP_DISPLAY_NAME,
125
+ text=result.error_text,
126
+ tone="error",
127
+ )
128
+ return
129
+ if result.sync_after:
130
+ window._sync_from_daemon()
131
+
132
+ def stop_runtime(self, window: "DataEngineWindow") -> None:
133
+ result = window.control_application.stop_pipeline(
134
+ paths=window.workspace_paths,
135
+ runtime_session=window.runtime_session,
136
+ selected_flow_group=None,
137
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
138
+ timeout=2.0,
139
+ )
140
+ if result.error_text is not None:
141
+ window._show_message_box_later(
142
+ title=APP_DISPLAY_NAME,
143
+ text=result.error_text,
144
+ tone="error",
145
+ )
146
+ return
147
+ if result.sync_after:
148
+ window._sync_from_daemon()
149
+
150
+ def toggle_runtime(self, window: "DataEngineWindow") -> None:
151
+ if window.runtime_session.runtime_active:
152
+ self.stop_runtime(window)
153
+ return
154
+ self.start_runtime(window)
155
+
156
+ def stop_pipeline(self, window: "DataEngineWindow") -> None:
157
+ card = window.flow_cards.get(window.selected_flow_name or "")
158
+ result = window.control_application.stop_pipeline(
159
+ paths=window.workspace_paths,
160
+ runtime_session=window.runtime_session,
161
+ selected_flow_group=card.group if card is not None else None,
162
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
163
+ timeout=2.0,
164
+ )
165
+ if result.error_text is not None:
166
+ window._show_message_box_later(
167
+ title=APP_DISPLAY_NAME,
168
+ text=result.error_text,
169
+ tone="error",
170
+ )
171
+ return
172
+ if result.sync_after:
173
+ window._sync_from_daemon()
174
+
175
+ def finish_run(self, window: "DataEngineWindow", flow_name: object, results: object, error: object) -> None:
176
+ assert isinstance(flow_name, str)
177
+ card = window.flow_cards.get(flow_name)
178
+ group_name = card.group if card is not None else next(
179
+ (run.group_name for run in window.runtime_session.manual_runs if run.flow_name == flow_name),
180
+ None,
181
+ )
182
+ stop_event = window.manual_flow_stop_events.pop(group_name, None)
183
+ stop_requested = stop_event.is_set() if stop_event is not None else False
184
+ if stop_event is not None:
185
+ stop_event.clear()
186
+ completion = self.runtime_application.complete_manual_run(
187
+ runtime_session=window.runtime_session,
188
+ flow_name=flow_name,
189
+ group_name=group_name,
190
+ flow_mode=card.mode if card is not None else "manual",
191
+ results=results,
192
+ error=error,
193
+ stop_requested=stop_requested,
194
+ )
195
+ window.runtime_session = completion.runtime_session
196
+ window.flow_controller.set_flow_states(window, completion.state_updates)
197
+ for message in completion.log_messages:
198
+ window._append_log_line(message.text, flow_name=message.flow_name)
199
+ if completion.capture_results:
200
+ window._capture_step_outputs(flow_name, results)
201
+ if completion.normalize_operations:
202
+ window._normalize_completed_operation_rows(flow_name)
203
+ if completion.render_durations:
204
+ window._render_operation_durations(flow_name)
205
+ if completion.show_error_text is not None:
206
+ window._show_message_box_later(
207
+ title=APP_DISPLAY_NAME,
208
+ text=completion.show_error_text,
209
+ tone="error",
210
+ )
211
+ window.flow_controller.refresh_action_buttons(window)
212
+
213
+ def finish_runtime(self, window: "DataEngineWindow", flow_names: object, results: object, error: object) -> None:
214
+ active_runtime_flow_names = tuple(flow_names) if isinstance(flow_names, tuple) else window.runtime_session.active_runtime_flow_names
215
+ runtime_stop_requested = window.engine_runtime_stop_event.is_set()
216
+ flow_stop_requested = window.engine_flow_stop_event.is_set()
217
+ completion = self.runtime_application.complete_engine_run(
218
+ runtime_session=window.runtime_session,
219
+ flow_names=active_runtime_flow_names,
220
+ flow_modes_by_name={
221
+ flow_name: (window.flow_cards[flow_name].mode if flow_name in window.flow_cards else None)
222
+ for flow_name in active_runtime_flow_names
223
+ },
224
+ error=error,
225
+ runtime_stop_requested=runtime_stop_requested,
226
+ flow_stop_requested=flow_stop_requested,
227
+ )
228
+ window.runtime_session = completion.runtime_session
229
+ window.engine_runtime_stop_event.clear()
230
+ window.engine_flow_stop_event.clear()
231
+ window.flow_controller.set_flow_states(window, completion.state_updates)
232
+ for message in completion.log_messages:
233
+ window._append_log_line(message.text, flow_name=message.flow_name)
234
+ for failed_flow_name in completion.failed_flow_names:
235
+ window.flow_controller.set_flow_state(window, failed_flow_name, "failed")
236
+ window.flow_controller.refresh_action_buttons(window)
237
+
238
+ def is_group_active(self, window: "DataEngineWindow", group_name: str) -> bool:
239
+ return window.runtime_session.is_group_active(
240
+ group_name,
241
+ {flow_name: card.group for flow_name, card in window.flow_cards.items()},
242
+ )
243
+
244
+
245
+ __all__ = ["GuiRuntimeController"]