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,349 @@
1
+ """Flow/workspace/detail controllers for the terminal UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from textual.widgets import ListView, Select, Static
8
+
9
+ from data_engine.application import FlowCatalogApplication, OperatorControlApplication, WorkspaceSessionApplication
10
+ from data_engine.services import LogService
11
+ from data_engine.domain import FlowRunState, FlowSummaryRow
12
+ from data_engine.views.text import render_operation_lines, render_run_group_lines, render_selected_flow_lines
13
+ from data_engine.ui.tui.widgets import FlowListItem, GroupHeaderListItem, InfoModal, RunGroupListItem
14
+
15
+ if TYPE_CHECKING:
16
+ from data_engine.ui.tui.app import DataEngineTui
17
+
18
+
19
+ class TuiFlowController:
20
+ """Compose narrower TUI flow collaborators behind one stable controller seam."""
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ workspace_session_application: WorkspaceSessionApplication,
26
+ flow_catalog_application: FlowCatalogApplication,
27
+ control_application: OperatorControlApplication,
28
+ log_service: LogService,
29
+ ) -> None:
30
+ self.workspace = _TuiWorkspaceCatalogController(
31
+ workspace_session_application=workspace_session_application,
32
+ flow_catalog_application=flow_catalog_application,
33
+ control_application=control_application,
34
+ )
35
+ self.presentation = _TuiFlowPresentationController(
36
+ control_application=control_application,
37
+ log_service=log_service,
38
+ )
39
+
40
+ def action_refresh_flows(self, window: "DataEngineTui") -> None:
41
+ self.workspace.action_refresh_flows(window, self.presentation)
42
+
43
+ def action_run_selected(self, window: "DataEngineTui") -> None:
44
+ self.presentation.action_run_selected(window)
45
+
46
+ def action_start_engine(self, window: "DataEngineTui") -> None:
47
+ self.presentation.action_start_engine(window)
48
+
49
+ def action_stop_engine(self, window: "DataEngineTui") -> None:
50
+ self.presentation.action_stop_engine(window)
51
+
52
+ def action_view_config(self, window: "DataEngineTui") -> None:
53
+ self.presentation.action_view_config(window)
54
+
55
+ def action_clear_flow_log(self, window: "DataEngineTui") -> None:
56
+ self.presentation.action_clear_flow_log(window)
57
+
58
+ def action_view_log(self, window: "DataEngineTui") -> None:
59
+ self.presentation.action_view_log(window)
60
+
61
+ def load_flows(self, window: "DataEngineTui") -> None:
62
+ self.workspace.load_flows(window, self.presentation)
63
+
64
+ def reload_workspace_options(self, window: "DataEngineTui") -> None:
65
+ self.workspace.reload_workspace_options(window)
66
+
67
+ def switch_workspace(self, window: "DataEngineTui", workspace_id: str) -> None:
68
+ self.workspace.switch_workspace(window, workspace_id, self.presentation)
69
+
70
+ def render_selected_flow(self, window: "DataEngineTui") -> None:
71
+ self.presentation.render_selected_flow(window)
72
+
73
+ def selected_run_group(self, window: "DataEngineTui") -> "FlowRunState | None":
74
+ return self.presentation.selected_run_group(window)
75
+
76
+
77
+ class _TuiWorkspaceCatalogController:
78
+ """Own TUI workspace binding and catalog refresh orchestration."""
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ workspace_session_application: WorkspaceSessionApplication,
84
+ flow_catalog_application: FlowCatalogApplication,
85
+ control_application: OperatorControlApplication,
86
+ ) -> None:
87
+ self.workspace_session_application = workspace_session_application
88
+ self.flow_catalog_application = flow_catalog_application
89
+ self.control_application = control_application
90
+
91
+ def action_refresh_flows(self, window: "DataEngineTui", presentation: "_TuiFlowPresentationController") -> None:
92
+ result = self.control_application.refresh_flows(
93
+ paths=window.workspace_paths,
94
+ runtime_session=window.runtime_session,
95
+ has_authored_workspace=window._has_authored_workspace(),
96
+ timeout=5.0,
97
+ )
98
+ if result.error_text is not None:
99
+ window._set_status(result.error_text)
100
+ return
101
+ if result.reload_catalog:
102
+ self.reload_workspace_options(window)
103
+ self.load_flows(window, presentation)
104
+ if result.sync_after:
105
+ window._sync_daemon_state()
106
+ if result.warning_text is not None:
107
+ window._set_status(result.warning_text)
108
+ return
109
+ if result.status_text is not None:
110
+ window._set_status(result.status_text)
111
+
112
+ def load_flows(self, window: "DataEngineTui", presentation_controller: "_TuiFlowPresentationController") -> None:
113
+ list_view = window.query_one("#flow-list", ListView)
114
+ list_view.clear()
115
+ missing_message = (
116
+ "Workspace collection root is not configured."
117
+ if not window.workspace_paths.workspace_configured
118
+ else "No flow modules discovered."
119
+ )
120
+ result = self.flow_catalog_application.load_workspace_catalog(
121
+ workspace_paths=window.workspace_paths,
122
+ current_state=window.flow_catalog_state,
123
+ missing_message=missing_message,
124
+ )
125
+ window.flow_catalog_state = result.catalog_state
126
+ presentation = self.flow_catalog_application.build_presentation(
127
+ catalog_state=window.flow_catalog_state,
128
+ )
129
+ if not result.loaded:
130
+ window.selected_flow_name = None
131
+ window.query_one("#detail-view", Static).update(window.flow_catalog_state.empty_message or missing_message)
132
+ window.query_one("#log-run-list", ListView).clear()
133
+ window._rebuild_runtime_snapshot()
134
+ return
135
+ for card in presentation.cards:
136
+ window.operation_tracker = window.operation_tracker.ensure_flow(card.name, card.operation_items)
137
+ for group_name, grouped in presentation.grouped_cards:
138
+ list_view.append(GroupHeaderListItem(group_name, len(grouped)))
139
+ for card in grouped:
140
+ list_view.append(FlowListItem(card, window.flow_states[card.name]))
141
+ if presentation.cards:
142
+ window.selected_flow_name = presentation.selected_flow_name
143
+ index = presentation.selected_list_index or 0
144
+ list_view.index = index
145
+ presentation_controller.render_selected_flow(window)
146
+ else:
147
+ window.selected_flow_name = None
148
+ window.query_one("#detail-view", Static).update("No flows discovered.")
149
+ window.query_one("#log-run-list", ListView).clear()
150
+ window._rebuild_runtime_snapshot()
151
+
152
+ def reload_workspace_options(self, window: "DataEngineTui") -> None:
153
+ window.workspace_session_state = self.workspace_session_application.refresh_session(
154
+ workspace_paths=window.workspace_paths,
155
+ override_root=window.workspace_collection_root_override,
156
+ )
157
+ current_id = window.workspace_session_state.current_workspace_id
158
+ workspace_ids = window.workspace_session_state.discovered_workspace_ids
159
+ selector = window.query_one("#workspace-select", Select)
160
+ window._workspace_switch_suppressed = True
161
+ try:
162
+ if not workspace_ids:
163
+ selector.set_options([("(no workspace)", Select.BLANK)])
164
+ selector.value = Select.BLANK
165
+ selector.disabled = True
166
+ else:
167
+ selector.set_options([(workspace_id, workspace_id) for workspace_id in workspace_ids])
168
+ if current_id in workspace_ids:
169
+ selector.value = current_id
170
+ else:
171
+ selector.value = workspace_ids[0]
172
+ selector.disabled = False
173
+ finally:
174
+ window._workspace_switch_suppressed = False
175
+
176
+ def switch_workspace(self, window: "DataEngineTui", workspace_id: str, presentation: "_TuiFlowPresentationController") -> None:
177
+ try:
178
+ window.runtime_binding_service.remove_client_session(window.runtime_binding, window.client_session_id)
179
+ except Exception:
180
+ pass
181
+ window.runtime_binding_service.close_binding(window.runtime_binding)
182
+ window.workspace_paths = window.workspace_service.resolve_paths(
183
+ workspace_id=workspace_id,
184
+ workspace_collection_root=window.workspace_collection_root_override,
185
+ )
186
+ binding = self.workspace_session_application.bind_workspace(
187
+ workspace_paths=window.workspace_paths,
188
+ override_root=window.workspace_collection_root_override,
189
+ )
190
+ window._operator_session_state = binding.operator_session
191
+ window.runtime_binding = window.runtime_binding_service.open_binding(window.workspace_paths)
192
+ window._register_client_session()
193
+ window.flow_cards = ()
194
+ window.flow_states = {}
195
+ window.selected_flow_name = None
196
+ window._last_daemon_spawn_attempt = 0.0
197
+ window._daemon_startup_in_progress = False
198
+ window.selected_run_key = None
199
+ window._last_rendered_flow_signature = None
200
+ window._last_run_list_signature = None
201
+ window._last_detail_signature = None
202
+ self.reload_workspace_options(window)
203
+ if window._has_authored_workspace():
204
+ window._ensure_daemon_started()
205
+ self.load_flows(window, presentation)
206
+ window._sync_daemon_state()
207
+ window._set_status(f"Switched to workspace {workspace_id}.")
208
+
209
+
210
+ class _TuiFlowPresentationController:
211
+ """Own TUI selected-flow, run-list, and action orchestration."""
212
+
213
+ def __init__(
214
+ self,
215
+ *,
216
+ control_application: OperatorControlApplication,
217
+ log_service: LogService,
218
+ ) -> None:
219
+ self.control_application = control_application
220
+ self.log_service = log_service
221
+
222
+ def action_run_selected(self, window: "DataEngineTui") -> None:
223
+ window._sync_daemon_state()
224
+ card = window._selected_card()
225
+ result = self.control_application.run_selected_flow(
226
+ paths=window.workspace_paths,
227
+ runtime_session=window.runtime_session,
228
+ selected_flow_name=card.name if card is not None else None,
229
+ selected_flow_valid=bool(card is not None and card.valid),
230
+ selected_flow_group=card.group if card is not None else None,
231
+ selected_flow_group_active=bool(card is not None and window.runtime_session.is_group_active(card.group, {flow.name: flow.group for flow in window.flow_cards})),
232
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
233
+ timeout=2.0,
234
+ )
235
+ if result.error_text is not None:
236
+ window._set_status(result.error_text)
237
+ return
238
+ if result.status_text is not None:
239
+ window._set_status(result.status_text)
240
+ if result.sync_after:
241
+ window._sync_daemon_state()
242
+
243
+ def action_start_engine(self, window: "DataEngineTui") -> None:
244
+ window._sync_daemon_state()
245
+ result = self.control_application.start_engine(
246
+ paths=window.workspace_paths,
247
+ runtime_session=window.runtime_session,
248
+ has_automated_flows=any(card.valid and card.mode in {"poll", "schedule"} for card in window.flow_cards),
249
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
250
+ timeout=2.0,
251
+ )
252
+ if result.error_text is not None:
253
+ window._set_status(result.error_text)
254
+ return
255
+ if result.status_text is not None:
256
+ window._set_status(result.status_text)
257
+ if result.sync_after:
258
+ window._sync_daemon_state()
259
+
260
+ def action_stop_engine(self, window: "DataEngineTui") -> None:
261
+ window._sync_daemon_state()
262
+ card = window._selected_card()
263
+ result = self.control_application.stop_pipeline(
264
+ paths=window.workspace_paths,
265
+ runtime_session=window.runtime_session,
266
+ selected_flow_group=card.group if card is not None else None,
267
+ blocked_status_text=window.workspace_control_state.blocked_status_text,
268
+ timeout=2.0,
269
+ )
270
+ if result.error_text is not None:
271
+ window._set_status(result.error_text)
272
+ return
273
+ if result.status_text is not None:
274
+ window._set_status(result.status_text)
275
+ if result.sync_after:
276
+ window._sync_daemon_state()
277
+
278
+ def action_view_config(self, window: "DataEngineTui") -> None:
279
+ card = window._selected_card()
280
+ if card is None:
281
+ window._set_status("Select one flow first.")
282
+ return
283
+ lines = [card.title]
284
+ if card.description:
285
+ lines.extend(["", card.description])
286
+ lines.extend([""])
287
+ lines.extend(f"{row.label}: {row.value}" for row in FlowSummaryRow.rows_for_flow(card, window.flow_states))
288
+ window.push_screen(InfoModal(title=card.title, body="\n".join(lines)))
289
+
290
+ def action_clear_flow_log(self, window: "DataEngineTui") -> None:
291
+ if window.selected_flow_name is None:
292
+ return
293
+ self.log_service.clear_flow(window.runtime_binding.log_store, window.selected_flow_name)
294
+ self.render_selected_flow(window)
295
+ window._set_status(f"Cleared log history for {window.selected_flow_name}.")
296
+
297
+ def action_view_log(self, window: "DataEngineTui") -> None:
298
+ run_group = self.selected_run_group(window)
299
+ if run_group is None:
300
+ window._set_status("Select one run first.")
301
+ return
302
+ window._show_run_group_modal(run_group)
303
+
304
+ def render_selected_flow(self, window: "DataEngineTui") -> None:
305
+ card = window._selected_card()
306
+ detail = window.query_one("#detail-view", Static)
307
+ run_list = window.query_one("#log-run-list", ListView)
308
+ run_groups = self.log_service.runs_for_flow(window.runtime_binding.log_store, card.name) if card is not None else ()
309
+ presentation = window.detail_application.build_selected_flow_presentation(
310
+ card=card,
311
+ tracker=window.operation_tracker,
312
+ flow_states=window.flow_states,
313
+ run_groups=tuple(run_groups),
314
+ selected_run_key=window.selected_run_key,
315
+ )
316
+ window.selected_run_key = presentation.selected_run_key
317
+ if presentation.detail_state is None:
318
+ detail.update(presentation.empty_text)
319
+ run_list.clear()
320
+ window._last_run_list_signature = ()
321
+ return
322
+ detail_lines = render_selected_flow_lines(card, window.operation_tracker)
323
+ detail.update("\n".join(detail_lines))
324
+ signature = presentation.run_group_signature
325
+ if signature != window._last_run_list_signature:
326
+ run_list.clear()
327
+ for run_group in presentation.visible_run_groups:
328
+ run_list.append(RunGroupListItem(run_group))
329
+ window._last_run_list_signature = signature
330
+ else:
331
+ visible_items = [child for child in run_list.children if isinstance(child, RunGroupListItem)]
332
+ for item, run_group in zip(visible_items, presentation.visible_run_groups):
333
+ item.refresh_view(run_group)
334
+
335
+ def selected_run_group(self, window: "DataEngineTui") -> "FlowRunState | None":
336
+ card = window._selected_card()
337
+ run_groups = self.log_service.runs_for_flow(window.runtime_binding.log_store, card.name) if card is not None else ()
338
+ presentation = window.detail_application.build_selected_flow_presentation(
339
+ card=card,
340
+ tracker=window.operation_tracker,
341
+ flow_states=window.flow_states,
342
+ run_groups=tuple(run_groups),
343
+ selected_run_key=window.selected_run_key,
344
+ )
345
+ window.selected_run_key = presentation.selected_run_key
346
+ return presentation.selected_run_group
347
+
348
+
349
+ __all__ = ["TuiFlowController"]
@@ -0,0 +1,167 @@
1
+ """Runtime/daemon controllers for the terminal UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from typing import TYPE_CHECKING
7
+
8
+ from textual.css.query import NoMatches
9
+ from textual.widgets import Button, ListView, Select, Static
10
+
11
+ from data_engine.application import RuntimeApplication
12
+ from data_engine.services import DaemonService, LogService
13
+ from data_engine.domain import RuntimeSessionState, WorkspaceControlState
14
+ from data_engine.views import TuiActionState, WORKSPACE_UNAVAILABLE_TEXT, surface_control_status_text
15
+ from data_engine.ui.tui.widgets import FlowListItem
16
+
17
+ if TYPE_CHECKING:
18
+ from data_engine.ui.tui.app import DataEngineTui
19
+
20
+
21
+ class TuiRuntimeController:
22
+ """Own daemon/runtime orchestration for the terminal UI."""
23
+
24
+ def __init__(
25
+ self,
26
+ *,
27
+ runtime_application: RuntimeApplication,
28
+ daemon_service: DaemonService,
29
+ log_service: LogService,
30
+ ) -> None:
31
+ self.runtime_application = runtime_application
32
+ self.daemon_service = daemon_service
33
+ self.log_service = log_service
34
+
35
+ def refresh_flow_list_items(self, window: "DataEngineTui") -> None:
36
+ list_view = window.query_one("#flow-list", ListView)
37
+ for child in list_view.children:
38
+ if isinstance(child, FlowListItem):
39
+ child.refresh_view(window.flow_states.get(child.card.name, child.card.state))
40
+
41
+ def refresh_buttons(self, window: "DataEngineTui") -> None:
42
+ action_state = TuiActionState.from_context(
43
+ window.action_state_application.build_action_context(
44
+ card=window._selected_card(),
45
+ flow_states=window.flow_states,
46
+ runtime_session=window.runtime_session,
47
+ flow_groups_by_name={card.name: card.group for card in window.flow_cards},
48
+ active_flow_states=window._ACTIVE_FLOW_STATES,
49
+ has_logs=bool(
50
+ window.selected_flow_name is not None
51
+ and self.log_service.entries_for_flow(window.runtime_binding.log_store, window.selected_flow_name)
52
+ ),
53
+ has_automated_flows=any(card.valid and card.mode in {"poll", "schedule"} for card in window.flow_cards),
54
+ workspace_available=window._has_authored_workspace(),
55
+ selected_run_group_present=window.flow_controller.selected_run_group(window) is not None,
56
+ )
57
+ )
58
+ window.query_one("#refresh", Button).disabled = action_state.refresh_disabled
59
+ window.query_one("#run-once", Button).disabled = action_state.run_once_disabled
60
+ window.query_one("#start-engine", Button).disabled = action_state.start_engine_disabled
61
+ window.query_one("#stop-engine", Button).disabled = action_state.stop_engine_disabled
62
+ window.query_one("#view-config", Button).disabled = action_state.view_config_disabled
63
+ window.query_one("#view-log", Button).disabled = action_state.view_log_disabled
64
+ window.query_one("#clear-flow-log", Button).disabled = action_state.clear_flow_log_disabled
65
+ window.query_one("#workspace-select", Select).disabled = action_state.workspace_select_disabled
66
+
67
+ def sync_daemon_state(self, window: "DataEngineTui") -> None:
68
+ if not window._has_authored_workspace():
69
+ window.runtime_session = RuntimeSessionState.empty()
70
+ window.workspace_control_state = WorkspaceControlState.empty()
71
+ window.flow_controller.reload_workspace_options(window)
72
+ window.flow_controller.load_flows(window)
73
+ try:
74
+ window.query_one("#control-status", Static).update(WORKSPACE_UNAVAILABLE_TEXT)
75
+ except NoMatches:
76
+ return
77
+ return
78
+ try:
79
+ live = self.daemon_service.is_live(window.workspace_paths)
80
+ except Exception:
81
+ live = False
82
+ if not live:
83
+ self.ensure_daemon_started(window)
84
+ sync_state = self.runtime_application.sync_state(
85
+ paths=window.workspace_paths,
86
+ daemon_manager=window.runtime_binding.daemon_manager,
87
+ flow_cards=window.flow_cards,
88
+ runtime_ledger=window.runtime_binding.runtime_ledger,
89
+ daemon_startup_in_progress=window._daemon_startup_in_progress,
90
+ )
91
+ window.runtime_session = sync_state.runtime_session
92
+ window.workspace_control_state = sync_state.workspace_control_state
93
+ try:
94
+ window.query_one("#control-status", Static).update(
95
+ surface_control_status_text(window.workspace_control_state.control_status_text)
96
+ )
97
+ except NoMatches:
98
+ return
99
+ self.rebuild_runtime_snapshot(window)
100
+
101
+ def ensure_daemon_started(self, window: "DataEngineTui") -> bool:
102
+ if not window._has_authored_workspace():
103
+ return False
104
+ try:
105
+ if self.daemon_service.is_live(window.workspace_paths):
106
+ return True
107
+ except Exception:
108
+ pass
109
+ if window._daemon_startup_in_progress:
110
+ return False
111
+ now = window._monotonic()
112
+ if now - window._last_daemon_spawn_attempt < 2.0:
113
+ return False
114
+ window._last_daemon_spawn_attempt = now
115
+ window._daemon_startup_in_progress = True
116
+ threading.Thread(target=window._start_daemon_worker, daemon=True).start()
117
+ return False
118
+
119
+ def start_daemon_worker(self, window: "DataEngineTui") -> None:
120
+ success = False
121
+ error_text = ""
122
+ spawn_result = self.runtime_application.spawn_daemon(window.workspace_paths)
123
+ if not spawn_result.ok:
124
+ error_text = spawn_result.error
125
+ else:
126
+ success = self.daemon_service.is_live(window.workspace_paths)
127
+ if not success and not error_text:
128
+ error_text = "Daemon startup did not provide any additional error details."
129
+ window.call_from_thread(window._finish_daemon_startup, success, error_text)
130
+
131
+ def finish_daemon_startup(self, window: "DataEngineTui", success: bool, error_text: str) -> None:
132
+ window._daemon_startup_in_progress = False
133
+ if success:
134
+ self.sync_daemon_state(window)
135
+ return
136
+ if error_text:
137
+ window._set_status(error_text)
138
+ else:
139
+ window._set_status("Daemon startup did not provide any additional error details.")
140
+ self.sync_daemon_state(window)
141
+
142
+ def rebuild_runtime_snapshot(self, window: "DataEngineTui") -> None:
143
+ self.log_service.reload(window.runtime_binding.log_store)
144
+ snapshot = self.runtime_application.build_runtime_snapshot(
145
+ flow_cards=window.flow_cards,
146
+ log_entries=self.log_service.all_entries(window.runtime_binding.log_store),
147
+ runtime_session=window.runtime_session,
148
+ now=window._monotonic(),
149
+ )
150
+ refresh_plan = self.runtime_application.plan_flow_state_refresh(
151
+ previous_states=window.flow_states,
152
+ next_states=snapshot.flow_states,
153
+ runtime_session=window.runtime_session,
154
+ )
155
+ window.operation_tracker = snapshot.operation_tracker
156
+ states_changed = refresh_plan.signature != window._last_rendered_flow_signature
157
+ window.flow_states = refresh_plan.flow_states
158
+ if not window.runtime_session.workspace_owned:
159
+ window._set_status(window.workspace_control_state.blocked_status_text)
160
+ if states_changed:
161
+ self.refresh_flow_list_items(window)
162
+ window._last_rendered_flow_signature = refresh_plan.signature
163
+ self.refresh_buttons(window)
164
+ window.flow_controller.render_selected_flow(window)
165
+
166
+
167
+ __all__ = ["TuiRuntimeController"]
@@ -0,0 +1,34 @@
1
+ """Runtime/logging helpers for the terminal UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from queue import Queue
7
+
8
+ from data_engine.domain import FlowLogEntry, format_log_line, parse_runtime_event
9
+
10
+
11
+ class QueueLogHandler(logging.Handler):
12
+ """Logging handler that forwards runtime lines into a queue."""
13
+
14
+ def __init__(self, queue: Queue[FlowLogEntry]) -> None:
15
+ super().__init__(level=logging.INFO)
16
+ self.queue = queue
17
+
18
+ def emit(self, record: logging.LogRecord) -> None:
19
+ try:
20
+ event = parse_runtime_event(record)
21
+ kind = "flow" if event is not None and event.flow_name is not None else "system"
22
+ self.queue.put_nowait(
23
+ FlowLogEntry(
24
+ line=format_log_line(record),
25
+ kind=kind,
26
+ event=event,
27
+ flow_name=event.flow_name if event is not None else None,
28
+ )
29
+ )
30
+ except Exception:
31
+ self.handleError(record)
32
+
33
+
34
+ __all__ = ["QueueLogHandler"]