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.
- data_engine/__init__.py +37 -0
- data_engine/application/__init__.py +39 -0
- data_engine/application/actions.py +42 -0
- data_engine/application/catalog.py +151 -0
- data_engine/application/control.py +213 -0
- data_engine/application/details.py +73 -0
- data_engine/application/runtime.py +449 -0
- data_engine/application/workspace.py +62 -0
- data_engine/authoring/__init__.py +14 -0
- data_engine/authoring/builder.py +31 -0
- data_engine/authoring/execution/__init__.py +6 -0
- data_engine/authoring/execution/app.py +6 -0
- data_engine/authoring/execution/context.py +82 -0
- data_engine/authoring/execution/continuous.py +176 -0
- data_engine/authoring/execution/grouped.py +106 -0
- data_engine/authoring/execution/logging.py +83 -0
- data_engine/authoring/execution/polling.py +135 -0
- data_engine/authoring/execution/runner.py +210 -0
- data_engine/authoring/execution/single.py +171 -0
- data_engine/authoring/flow.py +361 -0
- data_engine/authoring/helpers.py +160 -0
- data_engine/authoring/model.py +59 -0
- data_engine/authoring/primitives.py +430 -0
- data_engine/authoring/services.py +42 -0
- data_engine/devtools/__init__.py +3 -0
- data_engine/devtools/project_ast_map.py +503 -0
- data_engine/docs/__init__.py +1 -0
- data_engine/docs/sphinx_source/_static/custom.css +13 -0
- data_engine/docs/sphinx_source/api.rst +42 -0
- data_engine/docs/sphinx_source/conf.py +37 -0
- data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
- data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
- data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
- data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
- data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
- data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
- data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
- data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
- data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
- data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
- data_engine/docs/sphinx_source/guides/project-map.md +118 -0
- data_engine/docs/sphinx_source/guides/recipes.md +268 -0
- data_engine/docs/sphinx_source/index.rst +22 -0
- data_engine/domain/__init__.py +92 -0
- data_engine/domain/actions.py +69 -0
- data_engine/domain/catalog.py +128 -0
- data_engine/domain/details.py +214 -0
- data_engine/domain/diagnostics.py +56 -0
- data_engine/domain/errors.py +104 -0
- data_engine/domain/inspection.py +99 -0
- data_engine/domain/logs.py +118 -0
- data_engine/domain/operations.py +172 -0
- data_engine/domain/operator.py +72 -0
- data_engine/domain/runs.py +155 -0
- data_engine/domain/runtime.py +279 -0
- data_engine/domain/source_state.py +17 -0
- data_engine/domain/support.py +54 -0
- data_engine/domain/time.py +23 -0
- data_engine/domain/workspace.py +159 -0
- data_engine/flow_modules/__init__.py +1 -0
- data_engine/flow_modules/flow_module_compiler.py +179 -0
- data_engine/flow_modules/flow_module_loader.py +201 -0
- data_engine/helpers/__init__.py +25 -0
- data_engine/helpers/duckdb.py +705 -0
- data_engine/hosts/__init__.py +1 -0
- data_engine/hosts/daemon/__init__.py +23 -0
- data_engine/hosts/daemon/app.py +221 -0
- data_engine/hosts/daemon/bootstrap.py +69 -0
- data_engine/hosts/daemon/client.py +465 -0
- data_engine/hosts/daemon/commands.py +64 -0
- data_engine/hosts/daemon/composition.py +310 -0
- data_engine/hosts/daemon/constants.py +15 -0
- data_engine/hosts/daemon/entrypoints.py +97 -0
- data_engine/hosts/daemon/lifecycle.py +191 -0
- data_engine/hosts/daemon/manager.py +272 -0
- data_engine/hosts/daemon/ownership.py +126 -0
- data_engine/hosts/daemon/runtime_commands.py +188 -0
- data_engine/hosts/daemon/runtime_control.py +31 -0
- data_engine/hosts/daemon/server.py +84 -0
- data_engine/hosts/daemon/shared_state.py +147 -0
- data_engine/hosts/daemon/state_sync.py +101 -0
- data_engine/platform/__init__.py +1 -0
- data_engine/platform/identity.py +35 -0
- data_engine/platform/local_settings.py +146 -0
- data_engine/platform/theme.py +259 -0
- data_engine/platform/workspace_models.py +190 -0
- data_engine/platform/workspace_policy.py +333 -0
- data_engine/runtime/__init__.py +1 -0
- data_engine/runtime/file_watch.py +185 -0
- data_engine/runtime/ledger_models.py +116 -0
- data_engine/runtime/runtime_db.py +938 -0
- data_engine/runtime/shared_state.py +523 -0
- data_engine/services/__init__.py +49 -0
- data_engine/services/daemon.py +64 -0
- data_engine/services/daemon_state.py +40 -0
- data_engine/services/flow_catalog.py +102 -0
- data_engine/services/flow_execution.py +48 -0
- data_engine/services/ledger.py +85 -0
- data_engine/services/logs.py +65 -0
- data_engine/services/runtime_binding.py +105 -0
- data_engine/services/runtime_execution.py +126 -0
- data_engine/services/runtime_history.py +62 -0
- data_engine/services/settings.py +58 -0
- data_engine/services/shared_state.py +28 -0
- data_engine/services/theme.py +59 -0
- data_engine/services/workspace_provisioning.py +224 -0
- data_engine/services/workspaces.py +74 -0
- data_engine/ui/__init__.py +3 -0
- data_engine/ui/cli/__init__.py +19 -0
- data_engine/ui/cli/app.py +161 -0
- data_engine/ui/cli/commands_doctor.py +178 -0
- data_engine/ui/cli/commands_run.py +80 -0
- data_engine/ui/cli/commands_start.py +100 -0
- data_engine/ui/cli/commands_workspace.py +97 -0
- data_engine/ui/cli/dependencies.py +44 -0
- data_engine/ui/cli/parser.py +56 -0
- data_engine/ui/gui/__init__.py +25 -0
- data_engine/ui/gui/app.py +116 -0
- data_engine/ui/gui/bootstrap.py +487 -0
- data_engine/ui/gui/bootstrapper.py +140 -0
- data_engine/ui/gui/cache_models.py +23 -0
- data_engine/ui/gui/control_support.py +185 -0
- data_engine/ui/gui/controllers/__init__.py +6 -0
- data_engine/ui/gui/controllers/flows.py +439 -0
- data_engine/ui/gui/controllers/runtime.py +245 -0
- data_engine/ui/gui/dialogs/__init__.py +12 -0
- data_engine/ui/gui/dialogs/messages.py +88 -0
- data_engine/ui/gui/dialogs/previews.py +222 -0
- data_engine/ui/gui/helpers/__init__.py +62 -0
- data_engine/ui/gui/helpers/inspection.py +81 -0
- data_engine/ui/gui/helpers/lifecycle.py +112 -0
- data_engine/ui/gui/helpers/scroll.py +28 -0
- data_engine/ui/gui/helpers/theming.py +87 -0
- data_engine/ui/gui/icons/dark_light.svg +12 -0
- data_engine/ui/gui/icons/documentation.svg +1 -0
- data_engine/ui/gui/icons/failed.svg +3 -0
- data_engine/ui/gui/icons/group.svg +4 -0
- data_engine/ui/gui/icons/home.svg +2 -0
- data_engine/ui/gui/icons/manual.svg +2 -0
- data_engine/ui/gui/icons/poll.svg +2 -0
- data_engine/ui/gui/icons/schedule.svg +4 -0
- data_engine/ui/gui/icons/settings.svg +2 -0
- data_engine/ui/gui/icons/started.svg +3 -0
- data_engine/ui/gui/icons/success.svg +3 -0
- data_engine/ui/gui/icons/view-log.svg +3 -0
- data_engine/ui/gui/icons.py +50 -0
- data_engine/ui/gui/launcher.py +48 -0
- data_engine/ui/gui/presenters/__init__.py +72 -0
- data_engine/ui/gui/presenters/docs.py +140 -0
- data_engine/ui/gui/presenters/logs.py +58 -0
- data_engine/ui/gui/presenters/runtime_projection.py +29 -0
- data_engine/ui/gui/presenters/sidebar.py +88 -0
- data_engine/ui/gui/presenters/steps.py +148 -0
- data_engine/ui/gui/presenters/workspace.py +39 -0
- data_engine/ui/gui/presenters/workspace_binding.py +75 -0
- data_engine/ui/gui/presenters/workspace_settings.py +182 -0
- data_engine/ui/gui/preview_models.py +37 -0
- data_engine/ui/gui/render_support.py +241 -0
- data_engine/ui/gui/rendering/__init__.py +12 -0
- data_engine/ui/gui/rendering/artifacts.py +95 -0
- data_engine/ui/gui/rendering/icons.py +50 -0
- data_engine/ui/gui/runtime.py +47 -0
- data_engine/ui/gui/state_support.py +193 -0
- data_engine/ui/gui/support.py +214 -0
- data_engine/ui/gui/surface.py +209 -0
- data_engine/ui/gui/theme.py +720 -0
- data_engine/ui/gui/widgets/__init__.py +34 -0
- data_engine/ui/gui/widgets/config.py +41 -0
- data_engine/ui/gui/widgets/logs.py +62 -0
- data_engine/ui/gui/widgets/panels.py +507 -0
- data_engine/ui/gui/widgets/sidebar.py +130 -0
- data_engine/ui/gui/widgets/steps.py +84 -0
- data_engine/ui/tui/__init__.py +5 -0
- data_engine/ui/tui/app.py +222 -0
- data_engine/ui/tui/bootstrap.py +475 -0
- data_engine/ui/tui/bootstrapper.py +117 -0
- data_engine/ui/tui/controllers/__init__.py +6 -0
- data_engine/ui/tui/controllers/flows.py +349 -0
- data_engine/ui/tui/controllers/runtime.py +167 -0
- data_engine/ui/tui/runtime.py +34 -0
- data_engine/ui/tui/state_support.py +141 -0
- data_engine/ui/tui/support.py +63 -0
- data_engine/ui/tui/theme.py +204 -0
- data_engine/ui/tui/widgets.py +123 -0
- data_engine/views/__init__.py +109 -0
- data_engine/views/actions.py +80 -0
- data_engine/views/artifacts.py +58 -0
- data_engine/views/flow_display.py +69 -0
- data_engine/views/logs.py +54 -0
- data_engine/views/models.py +96 -0
- data_engine/views/presentation.py +133 -0
- data_engine/views/runs.py +62 -0
- data_engine/views/state.py +39 -0
- data_engine/views/status.py +13 -0
- data_engine/views/text.py +109 -0
- py_data_engine-0.1.0.dist-info/METADATA +330 -0
- py_data_engine-0.1.0.dist-info/RECORD +200 -0
- py_data_engine-0.1.0.dist-info/WHEEL +5 -0
- py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
- 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"]
|