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,12 @@
|
|
|
1
|
+
"""Dialog-layer helpers for the desktop UI."""
|
|
2
|
+
|
|
3
|
+
from data_engine.ui.gui.dialogs.messages import show_message_box, structured_error_content
|
|
4
|
+
from data_engine.ui.gui.dialogs.previews import show_config_preview, show_output_preview, show_run_log_preview
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"show_config_preview",
|
|
8
|
+
"show_message_box",
|
|
9
|
+
"show_output_preview",
|
|
10
|
+
"show_run_log_preview",
|
|
11
|
+
"structured_error_content",
|
|
12
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Message dialog helpers for the desktop UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QVBoxLayout
|
|
8
|
+
from data_engine.domain import StructuredErrorState
|
|
9
|
+
from data_engine.ui.gui.widgets import make_label_selectable
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def structured_error_content(text: str) -> StructuredErrorState | None:
|
|
16
|
+
"""Parse one developer-facing flow-module error into dialog sections when possible."""
|
|
17
|
+
return StructuredErrorState.parse(text)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def show_message_box(window: "DataEngineWindow", *, title: str, text: str, tone: str) -> None:
|
|
21
|
+
"""Show one simple application dialog for info/error messages."""
|
|
22
|
+
dialog = QDialog(window)
|
|
23
|
+
dialog.setWindowTitle(title)
|
|
24
|
+
dialog.setModal(True)
|
|
25
|
+
dialog.resize(520, 220)
|
|
26
|
+
layout = QVBoxLayout(dialog)
|
|
27
|
+
layout.setContentsMargins(20, 18, 20, 18)
|
|
28
|
+
layout.setSpacing(14)
|
|
29
|
+
|
|
30
|
+
structured = structured_error_content(text) if tone == "error" else None
|
|
31
|
+
title_label = QLabel(structured.title if structured is not None else title)
|
|
32
|
+
title_label.setObjectName("sectionTitle")
|
|
33
|
+
layout.addWidget(title_label)
|
|
34
|
+
|
|
35
|
+
if structured is None:
|
|
36
|
+
body_label = QLabel(text)
|
|
37
|
+
body_label.setWordWrap(True)
|
|
38
|
+
body_label.setObjectName("errorText" if tone == "error" else "bodyText")
|
|
39
|
+
make_label_selectable(body_label)
|
|
40
|
+
layout.addWidget(body_label, 1)
|
|
41
|
+
else:
|
|
42
|
+
summary_grid = QGridLayout()
|
|
43
|
+
summary_grid.setContentsMargins(0, 0, 0, 0)
|
|
44
|
+
summary_grid.setHorizontalSpacing(10)
|
|
45
|
+
summary_grid.setVerticalSpacing(6)
|
|
46
|
+
for row_index, field in enumerate(structured.fields):
|
|
47
|
+
label = QLabel(field.label)
|
|
48
|
+
label.setObjectName("fieldLabel")
|
|
49
|
+
value = QLabel(field.value)
|
|
50
|
+
value.setObjectName("fieldValue")
|
|
51
|
+
value.setWordWrap(True)
|
|
52
|
+
make_label_selectable(value)
|
|
53
|
+
summary_grid.addWidget(label, row_index, 0)
|
|
54
|
+
summary_grid.addWidget(value, row_index, 1)
|
|
55
|
+
layout.addLayout(summary_grid)
|
|
56
|
+
|
|
57
|
+
detail_label = QLabel("Error")
|
|
58
|
+
detail_label.setObjectName("fieldLabel")
|
|
59
|
+
layout.addWidget(detail_label)
|
|
60
|
+
|
|
61
|
+
detail_body = QTextEdit()
|
|
62
|
+
detail_body.setObjectName("outputPreviewText")
|
|
63
|
+
detail_body.setReadOnly(True)
|
|
64
|
+
detail_body.setPlainText(structured.detail)
|
|
65
|
+
detail_body.setMinimumHeight(88)
|
|
66
|
+
layout.addWidget(detail_body, 1)
|
|
67
|
+
|
|
68
|
+
raw_label = QLabel("Details")
|
|
69
|
+
raw_label.setObjectName("fieldLabel")
|
|
70
|
+
layout.addWidget(raw_label)
|
|
71
|
+
|
|
72
|
+
raw_body = QTextEdit()
|
|
73
|
+
raw_body.setObjectName("outputPreviewText")
|
|
74
|
+
raw_body.setReadOnly(True)
|
|
75
|
+
raw_body.setPlainText(structured.raw_text)
|
|
76
|
+
raw_body.setMinimumHeight(88)
|
|
77
|
+
layout.addWidget(raw_body, 1)
|
|
78
|
+
|
|
79
|
+
action_row = QHBoxLayout()
|
|
80
|
+
action_row.addStretch(1)
|
|
81
|
+
close_button = QPushButton("OK")
|
|
82
|
+
close_button.clicked.connect(dialog.accept)
|
|
83
|
+
action_row.addWidget(close_button)
|
|
84
|
+
layout.addLayout(action_row)
|
|
85
|
+
dialog.exec()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["show_message_box", "structured_error_content"]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Preview dialog helpers for the desktop UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QDialog,
|
|
11
|
+
QFrame,
|
|
12
|
+
QHBoxLayout,
|
|
13
|
+
QLabel,
|
|
14
|
+
QListWidget,
|
|
15
|
+
QListWidgetItem,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QVBoxLayout,
|
|
18
|
+
QWidget,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from data_engine.ui.gui.preview_models import ConfigPreviewRequest, OutputPreviewRequest, RunLogPreviewRequest
|
|
22
|
+
from data_engine.ui.gui.rendering import populate_output_preview, render_svg_icon_pixmap
|
|
23
|
+
from data_engine.ui.gui.widgets import build_config_value, make_label_selectable
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from data_engine.domain import FlowLogEntry, FlowRunState
|
|
27
|
+
from data_engine.views.models import QtFlowCard
|
|
28
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def show_run_log_preview(window: "DataEngineWindow", request: RunLogPreviewRequest) -> QDialog:
|
|
32
|
+
detail = request.detail
|
|
33
|
+
dialog = QDialog(window)
|
|
34
|
+
dialog.setWindowTitle("Run Log")
|
|
35
|
+
dialog.setObjectName("outputPreviewDialog")
|
|
36
|
+
dialog.resize(760, 520)
|
|
37
|
+
layout = QVBoxLayout(dialog)
|
|
38
|
+
layout.setContentsMargins(18, 18, 18, 18)
|
|
39
|
+
layout.setSpacing(12)
|
|
40
|
+
|
|
41
|
+
header = QFrame()
|
|
42
|
+
header.setObjectName("outputPreviewHeader")
|
|
43
|
+
header_layout = QVBoxLayout(header)
|
|
44
|
+
header_layout.setContentsMargins(14, 14, 14, 14)
|
|
45
|
+
header_layout.setSpacing(4)
|
|
46
|
+
|
|
47
|
+
title_label = QLabel(detail.display_label)
|
|
48
|
+
title_label.setObjectName("heroTitle")
|
|
49
|
+
header_layout.addWidget(title_label)
|
|
50
|
+
|
|
51
|
+
summary_parts = [detail.status.title()]
|
|
52
|
+
if detail.elapsed_seconds is not None:
|
|
53
|
+
summary_parts.append(window._format_seconds(detail.elapsed_seconds))
|
|
54
|
+
summary_label = QLabel(" • ".join(summary_parts))
|
|
55
|
+
summary_label.setObjectName("sectionMeta")
|
|
56
|
+
header_layout.addWidget(summary_label)
|
|
57
|
+
layout.addWidget(header)
|
|
58
|
+
|
|
59
|
+
log_list = QListWidget()
|
|
60
|
+
log_list.setObjectName("runLogList")
|
|
61
|
+
log_list.setSpacing(6)
|
|
62
|
+
for entry in request.run_group.entries:
|
|
63
|
+
item = QListWidgetItem(entry.line)
|
|
64
|
+
widget = _build_raw_log_entry_widget(window, entry, run_group=request.run_group)
|
|
65
|
+
item.setSizeHint(widget.sizeHint())
|
|
66
|
+
log_list.addItem(item)
|
|
67
|
+
log_list.setItemWidget(item, widget)
|
|
68
|
+
layout.addWidget(log_list, 1)
|
|
69
|
+
|
|
70
|
+
_present_dialog(dialog)
|
|
71
|
+
return dialog
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_raw_log_entry_widget(window: "DataEngineWindow", entry: "FlowLogEntry", *, run_group: "FlowRunState") -> QFrame:
|
|
75
|
+
frame = QFrame()
|
|
76
|
+
frame.setObjectName("rawLogRow")
|
|
77
|
+
frame.setMinimumHeight(40)
|
|
78
|
+
layout = QHBoxLayout(frame)
|
|
79
|
+
layout.setContentsMargins(8, 6, 8, 6)
|
|
80
|
+
layout.setSpacing(10)
|
|
81
|
+
|
|
82
|
+
timestamp = QLabel(entry.created_at_utc.astimezone().strftime("%I:%M:%S %p"))
|
|
83
|
+
timestamp.setObjectName("rawLogTimestamp")
|
|
84
|
+
make_label_selectable(timestamp)
|
|
85
|
+
layout.addWidget(timestamp, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
86
|
+
|
|
87
|
+
message = QLabel(window._format_raw_log_message(entry))
|
|
88
|
+
message.setObjectName("rawLogMessage")
|
|
89
|
+
message.setTextFormat(Qt.TextFormat.RichText)
|
|
90
|
+
message.setWordWrap(False)
|
|
91
|
+
make_label_selectable(message)
|
|
92
|
+
layout.addWidget(message, 1, Qt.AlignmentFlag.AlignVCenter)
|
|
93
|
+
|
|
94
|
+
event = entry.event
|
|
95
|
+
layout.addStretch(0)
|
|
96
|
+
inspect_slot = QWidget()
|
|
97
|
+
inspect_slot.setObjectName("rawLogInspectSlot")
|
|
98
|
+
inspect_slot.setFixedWidth(108)
|
|
99
|
+
inspect_slot_layout = QHBoxLayout(inspect_slot)
|
|
100
|
+
inspect_slot_layout.setContentsMargins(0, 0, 0, 0)
|
|
101
|
+
inspect_slot_layout.setSpacing(0)
|
|
102
|
+
inspect_slot_layout.addStretch(1)
|
|
103
|
+
inspect_button = QPushButton("Inspect")
|
|
104
|
+
inspect_button.setObjectName("inspectOutputButton")
|
|
105
|
+
inspect_button.setFixedWidth(96)
|
|
106
|
+
inspect_button.setEnabled(False)
|
|
107
|
+
inspect_slot_layout.addWidget(inspect_button, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
108
|
+
|
|
109
|
+
icon_slot = QWidget()
|
|
110
|
+
icon_slot.setObjectName("rawLogIconSlot")
|
|
111
|
+
icon_slot.setFixedWidth(20)
|
|
112
|
+
icon_slot_layout = QHBoxLayout(icon_slot)
|
|
113
|
+
icon_slot_layout.setContentsMargins(0, 0, 0, 0)
|
|
114
|
+
icon_slot_layout.setSpacing(0)
|
|
115
|
+
icon_slot_layout.addStretch(1)
|
|
116
|
+
|
|
117
|
+
if event is not None:
|
|
118
|
+
if event.status == "failed":
|
|
119
|
+
inspect_button.setEnabled(True)
|
|
120
|
+
inspect_button.clicked.connect(
|
|
121
|
+
lambda _checked=False, group=run_group, failed_entry=entry: window._show_run_error_details(group, failed_entry)
|
|
122
|
+
)
|
|
123
|
+
if event.status in {"started", "failed", "success"}:
|
|
124
|
+
status_name = "failed" if event.status == "failed" else "started" if event.status == "started" else "finished"
|
|
125
|
+
status_icon = QLabel()
|
|
126
|
+
status_icon.setObjectName("rawLogStatusIcon")
|
|
127
|
+
fill = window._LOG_ICON_COLORS.get(status_name, window._group_icon_color().name())
|
|
128
|
+
status_icon.setPixmap(
|
|
129
|
+
render_svg_icon_pixmap(
|
|
130
|
+
icon_name=window._LOG_ICON_NAMES[status_name],
|
|
131
|
+
size=14,
|
|
132
|
+
device_pixel_ratio=window.devicePixelRatioF(),
|
|
133
|
+
fill_color=fill,
|
|
134
|
+
default_fill_color=window._group_icon_color(),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
status_icon.setToolTip(event.status.title())
|
|
138
|
+
icon_slot_layout.addWidget(status_icon, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
139
|
+
|
|
140
|
+
layout.addWidget(inspect_slot, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
141
|
+
layout.addWidget(icon_slot, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
142
|
+
return frame
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def show_output_preview(window: "DataEngineWindow", request: OutputPreviewRequest) -> QDialog:
|
|
146
|
+
dialog = QDialog(window)
|
|
147
|
+
dialog.setWindowTitle("Inspect Output")
|
|
148
|
+
dialog.setObjectName("outputPreviewDialog")
|
|
149
|
+
dialog.resize(860, 520)
|
|
150
|
+
layout = QVBoxLayout(dialog)
|
|
151
|
+
layout.setContentsMargins(18, 18, 18, 18)
|
|
152
|
+
layout.setSpacing(12)
|
|
153
|
+
|
|
154
|
+
header = QFrame()
|
|
155
|
+
header.setObjectName("outputPreviewHeader")
|
|
156
|
+
header_layout = QVBoxLayout(header)
|
|
157
|
+
header_layout.setContentsMargins(14, 14, 14, 14)
|
|
158
|
+
header_layout.setSpacing(0)
|
|
159
|
+
|
|
160
|
+
path_label = QLabel(str(request.output_path))
|
|
161
|
+
path_label.setObjectName("outputPreviewPath")
|
|
162
|
+
path_label.setWordWrap(True)
|
|
163
|
+
make_label_selectable(path_label)
|
|
164
|
+
header_layout.addWidget(path_label)
|
|
165
|
+
layout.addWidget(header)
|
|
166
|
+
|
|
167
|
+
populate_output_preview(layout, request.output_path)
|
|
168
|
+
|
|
169
|
+
_present_dialog(dialog)
|
|
170
|
+
return dialog
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def show_config_preview(window: "DataEngineWindow", request: ConfigPreviewRequest) -> QDialog:
|
|
174
|
+
preview_state = request.preview
|
|
175
|
+
dialog = QDialog(window)
|
|
176
|
+
dialog.setWindowTitle(preview_state.title)
|
|
177
|
+
dialog.setObjectName("outputPreviewDialog")
|
|
178
|
+
dialog.resize(560, 420)
|
|
179
|
+
layout = QVBoxLayout(dialog)
|
|
180
|
+
layout.setContentsMargins(18, 18, 18, 18)
|
|
181
|
+
layout.setSpacing(12)
|
|
182
|
+
|
|
183
|
+
header = QFrame()
|
|
184
|
+
header.setObjectName("outputPreviewHeader")
|
|
185
|
+
header_layout = QVBoxLayout(header)
|
|
186
|
+
header_layout.setContentsMargins(14, 14, 14, 14)
|
|
187
|
+
header_layout.setSpacing(4)
|
|
188
|
+
|
|
189
|
+
title_label = QLabel(preview_state.title)
|
|
190
|
+
title_label.setObjectName("heroTitle")
|
|
191
|
+
title_label.setWordWrap(True)
|
|
192
|
+
header_layout.addWidget(title_label)
|
|
193
|
+
if preview_state.description:
|
|
194
|
+
description_label = QLabel(preview_state.description)
|
|
195
|
+
description_label.setObjectName("bodyText")
|
|
196
|
+
description_label.setWordWrap(True)
|
|
197
|
+
header_layout.addWidget(description_label)
|
|
198
|
+
layout.addWidget(header)
|
|
199
|
+
|
|
200
|
+
body = QFrame()
|
|
201
|
+
body.setObjectName("configPreviewBody")
|
|
202
|
+
body_layout = QVBoxLayout(body)
|
|
203
|
+
body_layout.setContentsMargins(14, 14, 14, 14)
|
|
204
|
+
body_layout.setSpacing(2)
|
|
205
|
+
for row in preview_state.summary.rows:
|
|
206
|
+
row_value = build_config_value(body_layout, row.label)
|
|
207
|
+
row_value.setText(row.value)
|
|
208
|
+
body_layout.addStretch(1)
|
|
209
|
+
layout.addWidget(body, 1)
|
|
210
|
+
|
|
211
|
+
_present_dialog(dialog)
|
|
212
|
+
return dialog
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _present_dialog(dialog: QDialog) -> None:
|
|
216
|
+
dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
217
|
+
dialog.show()
|
|
218
|
+
dialog.raise_()
|
|
219
|
+
dialog.activateWindow()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = ["show_config_preview", "show_output_preview", "show_run_log_preview"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Low-level helper functions for the desktop GUI shell."""
|
|
2
|
+
|
|
3
|
+
from data_engine.ui.gui.helpers.inspection import (
|
|
4
|
+
artifact_key_for_operation,
|
|
5
|
+
capture_step_outputs,
|
|
6
|
+
inspect_step_output,
|
|
7
|
+
is_inspectable_operation,
|
|
8
|
+
refresh_operation_buttons,
|
|
9
|
+
rehydrate_step_outputs_from_ledger,
|
|
10
|
+
show_config_preview,
|
|
11
|
+
show_output_preview,
|
|
12
|
+
)
|
|
13
|
+
from data_engine.ui.gui.helpers.lifecycle import (
|
|
14
|
+
is_last_process_ui_window,
|
|
15
|
+
register_client_session,
|
|
16
|
+
shutdown_daemon_on_close,
|
|
17
|
+
start_worker_thread,
|
|
18
|
+
unregister_client_session_and_check_for_shutdown,
|
|
19
|
+
wait_for_worker_threads,
|
|
20
|
+
)
|
|
21
|
+
from data_engine.ui.gui.helpers.scroll import update_operation_scroll_cues, update_sidebar_scroll_cues
|
|
22
|
+
from data_engine.ui.gui.helpers.theming import (
|
|
23
|
+
action_bar_icon,
|
|
24
|
+
apply_theme,
|
|
25
|
+
group_icon,
|
|
26
|
+
group_icon_color,
|
|
27
|
+
log_icon,
|
|
28
|
+
render_group_icon_pixmap,
|
|
29
|
+
render_svg_icon_pixmap,
|
|
30
|
+
sync_theme_to_system,
|
|
31
|
+
toggle_theme,
|
|
32
|
+
view_rail_icon,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"action_bar_icon",
|
|
37
|
+
"apply_theme",
|
|
38
|
+
"artifact_key_for_operation",
|
|
39
|
+
"capture_step_outputs",
|
|
40
|
+
"group_icon",
|
|
41
|
+
"group_icon_color",
|
|
42
|
+
"is_last_process_ui_window",
|
|
43
|
+
"inspect_step_output",
|
|
44
|
+
"is_inspectable_operation",
|
|
45
|
+
"log_icon",
|
|
46
|
+
"register_client_session",
|
|
47
|
+
"refresh_operation_buttons",
|
|
48
|
+
"rehydrate_step_outputs_from_ledger",
|
|
49
|
+
"render_group_icon_pixmap",
|
|
50
|
+
"render_svg_icon_pixmap",
|
|
51
|
+
"show_config_preview",
|
|
52
|
+
"show_output_preview",
|
|
53
|
+
"shutdown_daemon_on_close",
|
|
54
|
+
"start_worker_thread",
|
|
55
|
+
"sync_theme_to_system",
|
|
56
|
+
"toggle_theme",
|
|
57
|
+
"unregister_client_session_and_check_for_shutdown",
|
|
58
|
+
"update_operation_scroll_cues",
|
|
59
|
+
"update_sidebar_scroll_cues",
|
|
60
|
+
"view_rail_icon",
|
|
61
|
+
"wait_for_worker_threads",
|
|
62
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Output inspection helper functions for the desktop GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from data_engine.views.state import artifact_key_for_operation as artifact_key_for_operation_helper
|
|
9
|
+
from data_engine.views.state import capture_step_outputs as capture_step_outputs_helper
|
|
10
|
+
from data_engine.views.state import is_inspectable_operation as is_inspectable_operation_helper
|
|
11
|
+
from data_engine.domain import ConfigPreviewState
|
|
12
|
+
from data_engine.ui.gui.dialogs import show_config_preview as show_config_preview_dialog
|
|
13
|
+
from data_engine.ui.gui.dialogs import show_output_preview as show_output_preview_dialog
|
|
14
|
+
from data_engine.ui.gui.preview_models import ConfigPreviewRequest, OutputPreviewRequest
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_inspectable_operation(operation_name: str) -> bool:
|
|
21
|
+
return is_inspectable_operation_helper(operation_name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def artifact_key_for_operation(operation_name: str) -> str | None:
|
|
25
|
+
return artifact_key_for_operation_helper(operation_name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def capture_step_outputs(window: "DataEngineWindow", flow_name: str, results: object) -> None:
|
|
29
|
+
updated = capture_step_outputs_helper(
|
|
30
|
+
window.flow_cards[flow_name],
|
|
31
|
+
window.step_output_index.outputs_for(flow_name).outputs,
|
|
32
|
+
results,
|
|
33
|
+
)
|
|
34
|
+
window.step_output_index = window.step_output_index.with_flow_outputs(flow_name, updated)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def rehydrate_step_outputs_from_ledger(window: "DataEngineWindow") -> None:
|
|
38
|
+
window.step_output_index = window.runtime_history_service.rebuild_step_outputs(
|
|
39
|
+
window.runtime_binding.runtime_ledger,
|
|
40
|
+
window.flow_cards,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def refresh_operation_buttons(window: "DataEngineWindow", flow_name: str) -> None:
|
|
45
|
+
for row_widgets in window.operation_row_widgets:
|
|
46
|
+
if row_widgets.inspect_button is None:
|
|
47
|
+
continue
|
|
48
|
+
row_widgets.inspect_button.setEnabled(window.step_output_index.has_output(flow_name, row_widgets.operation_name))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def inspect_step_output(window: "DataEngineWindow", operation_name: str) -> None:
|
|
52
|
+
request = build_output_preview_request(window, operation_name)
|
|
53
|
+
if request is None:
|
|
54
|
+
window._show_message_box(
|
|
55
|
+
title="Inspect Output",
|
|
56
|
+
text="No output is available for this step yet.",
|
|
57
|
+
tone="info",
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
window._show_output_preview(request.operation_name, request.output_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def show_output_preview(window: "DataEngineWindow", operation_name: str, output_path: Path) -> None:
|
|
64
|
+
request = OutputPreviewRequest(operation_name=operation_name, output_path=output_path)
|
|
65
|
+
window.output_preview_dialog = show_output_preview_dialog(window, request)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def show_config_preview(window: "DataEngineWindow") -> None:
|
|
69
|
+
card = window.flow_cards.get(window.selected_flow_name or "")
|
|
70
|
+
preview_state = ConfigPreviewState.from_flow(card, window.flow_states)
|
|
71
|
+
window.config_preview_dialog = show_config_preview_dialog(window, ConfigPreviewRequest(preview=preview_state))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_output_preview_request(window: "DataEngineWindow", operation_name: str) -> OutputPreviewRequest | None:
|
|
75
|
+
"""Build one explicit output-preview request for the selected flow."""
|
|
76
|
+
if window.selected_flow_name is None:
|
|
77
|
+
return None
|
|
78
|
+
output_path = window.step_output_index.output_path(window.selected_flow_name, operation_name)
|
|
79
|
+
if output_path is None or not output_path.exists():
|
|
80
|
+
return None
|
|
81
|
+
return OutputPreviewRequest(operation_name=operation_name, output_path=output_path)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Lifecycle and worker helpers for the desktop GUI surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from time import monotonic
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import QApplication
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_client_session(window: "DataEngineWindow") -> None:
|
|
18
|
+
"""Register this UI process as one active local client for the workspace."""
|
|
19
|
+
window.runtime_binding_service.register_client_session(
|
|
20
|
+
window.runtime_binding,
|
|
21
|
+
client_id=window.client_session_id,
|
|
22
|
+
client_kind="ui",
|
|
23
|
+
pid=os.getpid(),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_last_process_ui_window(window: "DataEngineWindow") -> bool:
|
|
28
|
+
"""Return whether this is the last Data Engine window still open in this process."""
|
|
29
|
+
for widget in QApplication.topLevelWidgets():
|
|
30
|
+
if widget is window:
|
|
31
|
+
continue
|
|
32
|
+
if isinstance(widget, type(window)) and not getattr(widget, "ui_closing", False):
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def unregister_client_session_and_check_for_shutdown(
|
|
38
|
+
window: "DataEngineWindow",
|
|
39
|
+
*,
|
|
40
|
+
purge_process_ui_sessions: bool = False,
|
|
41
|
+
) -> bool:
|
|
42
|
+
"""Remove this UI session and return whether no local clients remain."""
|
|
43
|
+
try:
|
|
44
|
+
window.runtime_binding_service.remove_client_session(window.runtime_binding, window.client_session_id)
|
|
45
|
+
if purge_process_ui_sessions:
|
|
46
|
+
window.runtime_binding_service.purge_process_client_sessions(
|
|
47
|
+
window.runtime_binding,
|
|
48
|
+
client_kind="ui",
|
|
49
|
+
pid=os.getpid(),
|
|
50
|
+
)
|
|
51
|
+
remaining = window.runtime_binding_service.count_live_client_sessions(window.runtime_binding)
|
|
52
|
+
return remaining == 0
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def shutdown_daemon_on_close(window: "DataEngineWindow") -> None:
|
|
58
|
+
"""Best-effort local daemon shutdown when the last local client closes."""
|
|
59
|
+
client_error_type = window._daemon_client_error_type
|
|
60
|
+
try:
|
|
61
|
+
if not window._is_daemon_live(window.workspace_paths):
|
|
62
|
+
return
|
|
63
|
+
window._daemon_request(window.workspace_paths, {"command": "shutdown_daemon"}, timeout=1.5)
|
|
64
|
+
deadline = monotonic() + 2.0
|
|
65
|
+
while monotonic() < deadline:
|
|
66
|
+
if not window._is_daemon_live(window.workspace_paths):
|
|
67
|
+
break
|
|
68
|
+
time.sleep(0.05)
|
|
69
|
+
except client_error_type:
|
|
70
|
+
pass
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def start_worker_thread(window: "DataEngineWindow", *, target, args=()) -> None:
|
|
76
|
+
"""Start one tracked daemon worker thread for background UI tasks."""
|
|
77
|
+
thread = threading.Thread(target=run_tracked_worker, args=(window, target, args), daemon=True)
|
|
78
|
+
window._register_worker_thread(thread)
|
|
79
|
+
thread.start()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_tracked_worker(window: "DataEngineWindow", target, args) -> None:
|
|
83
|
+
"""Execute one worker target and remove its thread from the tracked set."""
|
|
84
|
+
current = threading.current_thread()
|
|
85
|
+
try:
|
|
86
|
+
target(*args)
|
|
87
|
+
finally:
|
|
88
|
+
window._discard_worker_thread(current)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def wait_for_worker_threads(window: "DataEngineWindow", *, timeout_seconds: float) -> None:
|
|
92
|
+
"""Wait briefly for tracked workers to honor stop requests before exit."""
|
|
93
|
+
deadline = time.monotonic() + max(timeout_seconds, 0.0)
|
|
94
|
+
for thread in window._worker_threads_snapshot():
|
|
95
|
+
remaining = deadline - time.monotonic()
|
|
96
|
+
if remaining <= 0:
|
|
97
|
+
break
|
|
98
|
+
is_alive = getattr(thread, "is_alive", None)
|
|
99
|
+
join = getattr(thread, "join", None)
|
|
100
|
+
alive = bool(is_alive()) if callable(is_alive) else False
|
|
101
|
+
if alive and callable(join):
|
|
102
|
+
join(timeout=min(remaining, 0.3))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"is_last_process_ui_window",
|
|
107
|
+
"register_client_session",
|
|
108
|
+
"shutdown_daemon_on_close",
|
|
109
|
+
"start_worker_thread",
|
|
110
|
+
"unregister_client_session_and_check_for_shutdown",
|
|
111
|
+
"wait_for_worker_threads",
|
|
112
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Scroll-surface helper functions for the desktop GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def update_operation_scroll_cues(window: "DataEngineWindow", *args) -> None:
|
|
12
|
+
del args
|
|
13
|
+
scrollbar = window.operation_scroll.verticalScrollBar()
|
|
14
|
+
maximum = scrollbar.maximum()
|
|
15
|
+
value = scrollbar.value()
|
|
16
|
+
has_overflow = maximum > 0
|
|
17
|
+
window.operation_top_cue.setVisible(has_overflow and value > 0)
|
|
18
|
+
window.operation_bottom_cue.setVisible(has_overflow and value < maximum)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def update_sidebar_scroll_cues(window: "DataEngineWindow", *args) -> None:
|
|
22
|
+
del args
|
|
23
|
+
scrollbar = window.sidebar_scroll.verticalScrollBar()
|
|
24
|
+
maximum = scrollbar.maximum()
|
|
25
|
+
value = scrollbar.value()
|
|
26
|
+
has_overflow = maximum > 0
|
|
27
|
+
window.sidebar_top_cue.setVisible(has_overflow and value > 0)
|
|
28
|
+
window.sidebar_bottom_cue.setVisible(has_overflow and value < maximum)
|