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,80 @@
|
|
|
1
|
+
"""Shared operator action-state view models across GUI and TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from data_engine.domain import OperatorActionContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class GuiActionState:
|
|
12
|
+
"""Button and control state for the desktop GUI surface."""
|
|
13
|
+
|
|
14
|
+
flow_run_label: str
|
|
15
|
+
flow_run_enabled: bool
|
|
16
|
+
flow_config_enabled: bool
|
|
17
|
+
engine_enabled: bool
|
|
18
|
+
engine_label: str
|
|
19
|
+
engine_state: str
|
|
20
|
+
refresh_enabled: bool
|
|
21
|
+
clear_flow_log_enabled: bool
|
|
22
|
+
request_control_visible: bool
|
|
23
|
+
request_control_enabled: bool
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_context(cls, context: OperatorActionContext) -> "GuiActionState":
|
|
27
|
+
"""Return the GUI action state derived from one operator action context."""
|
|
28
|
+
session = context.runtime_session
|
|
29
|
+
selected = context.selected_flow
|
|
30
|
+
active = session.runtime_active or session.runtime_stopping
|
|
31
|
+
return cls(
|
|
32
|
+
flow_run_label="Running..." if selected.running else "Run Once",
|
|
33
|
+
flow_run_enabled=selected.valid and not selected.group_active and session.control_available and context.workspace_available,
|
|
34
|
+
flow_config_enabled=selected.present,
|
|
35
|
+
engine_enabled=(
|
|
36
|
+
session.runtime_active
|
|
37
|
+
or (context.has_automated_flows and session.control_available and context.workspace_available)
|
|
38
|
+
) and not session.runtime_stopping,
|
|
39
|
+
engine_label="Stopping..." if session.runtime_stopping else "Stop Engine" if active else "Start Engine",
|
|
40
|
+
engine_state="running" if active else "stopped",
|
|
41
|
+
refresh_enabled=not session.runtime_active and not session.manual_run_active,
|
|
42
|
+
clear_flow_log_enabled=selected.present and selected.has_logs,
|
|
43
|
+
request_control_visible=True,
|
|
44
|
+
request_control_enabled=not session.workspace_owned,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TuiActionState:
|
|
50
|
+
"""Button and control state for the terminal UI surface."""
|
|
51
|
+
|
|
52
|
+
refresh_disabled: bool
|
|
53
|
+
run_once_disabled: bool
|
|
54
|
+
start_engine_disabled: bool
|
|
55
|
+
stop_engine_disabled: bool
|
|
56
|
+
view_config_disabled: bool
|
|
57
|
+
view_log_disabled: bool
|
|
58
|
+
clear_flow_log_disabled: bool
|
|
59
|
+
workspace_select_disabled: bool
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_context(cls, context: OperatorActionContext) -> "TuiActionState":
|
|
62
|
+
"""Return the TUI action state derived from one operator action context."""
|
|
63
|
+
session = context.runtime_session
|
|
64
|
+
busy = session.runtime_active or session.manual_run_active or session.runtime_stopping
|
|
65
|
+
return cls(
|
|
66
|
+
refresh_disabled=busy,
|
|
67
|
+
run_once_disabled=busy or not session.control_available or not context.workspace_available,
|
|
68
|
+
start_engine_disabled=busy or not session.control_available or not context.workspace_available,
|
|
69
|
+
stop_engine_disabled=not busy,
|
|
70
|
+
view_config_disabled=not context.selected_flow.present,
|
|
71
|
+
view_log_disabled=not context.selected_run_group_present,
|
|
72
|
+
clear_flow_log_disabled=not context.selected_flow.present,
|
|
73
|
+
workspace_select_disabled=False,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"GuiActionState",
|
|
79
|
+
"TuiActionState",
|
|
80
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared artifact-preview presentation decisions across operator surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import mimetypes
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ArtifactPreviewSpec:
|
|
12
|
+
"""Describe how one artifact should be previewed."""
|
|
13
|
+
|
|
14
|
+
kind: str
|
|
15
|
+
label: str
|
|
16
|
+
previewable: bool
|
|
17
|
+
placeholder_message: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def classify_artifact_preview(path: Path) -> ArtifactPreviewSpec:
|
|
21
|
+
"""Return the preview strategy for one output artifact."""
|
|
22
|
+
suffix = path.suffix.lower()
|
|
23
|
+
if suffix in {".parquet"}:
|
|
24
|
+
return ArtifactPreviewSpec(kind="parquet", label="Parquet table preview", previewable=True)
|
|
25
|
+
if suffix in {".xlsx", ".xls", ".xlsm", ".xlsb"}:
|
|
26
|
+
return ArtifactPreviewSpec(kind="excel", label="Excel table preview", previewable=True)
|
|
27
|
+
if suffix in {".pdf"}:
|
|
28
|
+
return ArtifactPreviewSpec(
|
|
29
|
+
kind="pdf",
|
|
30
|
+
label="PDF inspection",
|
|
31
|
+
previewable=False,
|
|
32
|
+
placeholder_message="PDF artifacts are recognized, but in-app PDF text inspection is not available yet.",
|
|
33
|
+
)
|
|
34
|
+
if is_text_artifact(path):
|
|
35
|
+
return ArtifactPreviewSpec(kind="text", label="Text preview", previewable=True)
|
|
36
|
+
return ArtifactPreviewSpec(
|
|
37
|
+
kind="unsupported",
|
|
38
|
+
label="Artifact inspection",
|
|
39
|
+
previewable=False,
|
|
40
|
+
placeholder_message="This artifact type is not previewable in the UI yet.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_text_artifact(path: Path) -> bool:
|
|
45
|
+
"""Return whether one artifact should use text-preview treatment."""
|
|
46
|
+
suffix = path.suffix.lower()
|
|
47
|
+
if suffix in {
|
|
48
|
+
".txt", ".log", ".md", ".json", ".csv", ".tsv", ".yaml", ".yml",
|
|
49
|
+
".xml", ".html", ".htm", ".sql", ".py", ".toml", ".ini", ".cfg",
|
|
50
|
+
}:
|
|
51
|
+
return True
|
|
52
|
+
guessed_type, _encoding = mimetypes.guess_type(path.name)
|
|
53
|
+
if guessed_type is None:
|
|
54
|
+
return False
|
|
55
|
+
return guessed_type.startswith("text/") or guessed_type in {"application/json", "application/xml", "application/x-yaml"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["ArtifactPreviewSpec", "classify_artifact_preview", "is_text_artifact"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared flow/group row display models across GUI and TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from data_engine.views.models import QtFlowCard
|
|
8
|
+
from data_engine.views.presentation import FlowGroupBucket, flow_secondary_text, group_label, group_secondary_text, state_dot, status_color_name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FlowRowDisplay:
|
|
13
|
+
"""Display metadata for one flow row in a list/tree surface."""
|
|
14
|
+
|
|
15
|
+
primary: str
|
|
16
|
+
secondary: str
|
|
17
|
+
state_color: str
|
|
18
|
+
dot: str
|
|
19
|
+
tooltip: str
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_card(cls, card: QtFlowCard, state: str, *, primary: str = "title") -> "FlowRowDisplay":
|
|
23
|
+
"""Return display metadata for one flow row."""
|
|
24
|
+
primary_text = card.title if primary == "title" else card.name
|
|
25
|
+
resolved_state = state if card.valid else "failed"
|
|
26
|
+
tooltip = f"{card.name} | {card.title} | {state}"
|
|
27
|
+
if card.group:
|
|
28
|
+
tooltip = f"{tooltip} | group={card.group}"
|
|
29
|
+
return cls(
|
|
30
|
+
primary=primary_text,
|
|
31
|
+
secondary=flow_secondary_text(card.mode, state),
|
|
32
|
+
state_color=status_color_name(state),
|
|
33
|
+
dot=state_dot(resolved_state),
|
|
34
|
+
tooltip=tooltip,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class GroupRowDisplay:
|
|
40
|
+
"""Display metadata for one grouped flow header."""
|
|
41
|
+
|
|
42
|
+
title: str
|
|
43
|
+
secondary: str
|
|
44
|
+
uppercase_title: str
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_group(
|
|
47
|
+
cls,
|
|
48
|
+
group_name: str,
|
|
49
|
+
entries: list[QtFlowCard] | tuple[QtFlowCard, ...],
|
|
50
|
+
flow_states: dict[str, str],
|
|
51
|
+
) -> "GroupRowDisplay":
|
|
52
|
+
"""Return display metadata for one flow group header."""
|
|
53
|
+
title = group_label(group_name)
|
|
54
|
+
return cls(
|
|
55
|
+
title=title,
|
|
56
|
+
secondary=group_secondary_text(list(entries), flow_states),
|
|
57
|
+
uppercase_title=title.upper(),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_bucket(cls, bucket: FlowGroupBucket, flow_states: dict[str, str]) -> "GroupRowDisplay":
|
|
62
|
+
"""Return display metadata for one grouped flow bucket."""
|
|
63
|
+
return cls.from_group(bucket.group_name, bucket.entries, flow_states)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"FlowRowDisplay",
|
|
68
|
+
"GroupRowDisplay",
|
|
69
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared log storage helpers for Data Engine operator surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from data_engine.domain import FlowLogEntry, FlowRunState, LogKind, RuntimeStepEvent
|
|
6
|
+
|
|
7
|
+
CollapsedLogKey = tuple[str, str, str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FlowLogStore:
|
|
11
|
+
"""Keep operator log history and expose per-flow filtered views."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, entries: tuple[FlowLogEntry, ...] = ()) -> None:
|
|
14
|
+
self._entries: list[FlowLogEntry] = list(entries)
|
|
15
|
+
|
|
16
|
+
def append_entry(self, entry: FlowLogEntry) -> None:
|
|
17
|
+
self._entries.append(entry)
|
|
18
|
+
|
|
19
|
+
def append_line(self, line: str, *, kind: LogKind, flow_name: str | None = None) -> FlowLogEntry:
|
|
20
|
+
entry = FlowLogEntry(line=line, kind=kind, flow_name=flow_name)
|
|
21
|
+
self.append_entry(entry)
|
|
22
|
+
return entry
|
|
23
|
+
|
|
24
|
+
def clear(self) -> None:
|
|
25
|
+
self._entries.clear()
|
|
26
|
+
|
|
27
|
+
def clear_flow(self, flow_name: str | None) -> None:
|
|
28
|
+
if flow_name is None:
|
|
29
|
+
return
|
|
30
|
+
self._entries = [
|
|
31
|
+
entry
|
|
32
|
+
for entry in self._entries
|
|
33
|
+
if not (entry.kind == "flow" and entry.flow_name == flow_name)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def entries_for_flow(self, flow_name: str | None) -> tuple[FlowLogEntry, ...]:
|
|
37
|
+
if flow_name is None:
|
|
38
|
+
return ()
|
|
39
|
+
return tuple(entry for entry in self._entries if entry.kind == "flow" and entry.flow_name == flow_name)
|
|
40
|
+
|
|
41
|
+
def runs_for_flow(self, flow_name: str | None) -> tuple[FlowRunState, ...]:
|
|
42
|
+
entries = self.entries_for_flow(flow_name)
|
|
43
|
+
if not entries:
|
|
44
|
+
return ()
|
|
45
|
+
return FlowRunState.group_entries(entries)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"CollapsedLogKey",
|
|
49
|
+
"FlowLogStore",
|
|
50
|
+
"FlowLogEntry",
|
|
51
|
+
"FlowRunState",
|
|
52
|
+
"LogKind",
|
|
53
|
+
"RuntimeStepEvent",
|
|
54
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Shared UI-facing card models and small display helpers across Data Engine surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from data_engine.domain import FlowCatalogEntry, default_flow_state, flow_category
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from data_engine.services.flow_catalog import FlowCatalogService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class QtFlowCard:
|
|
17
|
+
"""Display model for one discovered flow."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
group: str | None
|
|
21
|
+
title: str
|
|
22
|
+
description: str
|
|
23
|
+
source_root: str
|
|
24
|
+
target_root: str
|
|
25
|
+
mode: str
|
|
26
|
+
interval: str
|
|
27
|
+
operations: str
|
|
28
|
+
operation_items: tuple[str, ...]
|
|
29
|
+
state: str
|
|
30
|
+
valid: bool
|
|
31
|
+
category: str
|
|
32
|
+
error: str = ""
|
|
33
|
+
|
|
34
|
+
def qt_flow_card_from_entry(entry: FlowCatalogEntry) -> QtFlowCard:
|
|
35
|
+
"""Map one catalog entry into a shared surface card."""
|
|
36
|
+
return QtFlowCard(
|
|
37
|
+
name=entry.name,
|
|
38
|
+
group=entry.group,
|
|
39
|
+
title=entry.title,
|
|
40
|
+
description=entry.description,
|
|
41
|
+
source_root=entry.source_root,
|
|
42
|
+
target_root=entry.target_root,
|
|
43
|
+
mode=entry.mode,
|
|
44
|
+
interval=entry.interval,
|
|
45
|
+
operations=entry.operations,
|
|
46
|
+
operation_items=entry.operation_items,
|
|
47
|
+
state=entry.state,
|
|
48
|
+
valid=entry.valid,
|
|
49
|
+
category=entry.category,
|
|
50
|
+
error=entry.error,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def flow_catalog_entry_from_qt_card(card: QtFlowCard) -> FlowCatalogEntry:
|
|
55
|
+
"""Map one shared surface card back into a catalog entry."""
|
|
56
|
+
return FlowCatalogEntry(
|
|
57
|
+
name=card.name,
|
|
58
|
+
group=card.group,
|
|
59
|
+
title=card.title,
|
|
60
|
+
description=card.description,
|
|
61
|
+
source_root=card.source_root,
|
|
62
|
+
target_root=card.target_root,
|
|
63
|
+
mode=card.mode,
|
|
64
|
+
interval=card.interval,
|
|
65
|
+
operations=card.operations,
|
|
66
|
+
operation_items=card.operation_items,
|
|
67
|
+
state=card.state,
|
|
68
|
+
valid=card.valid,
|
|
69
|
+
category=card.category,
|
|
70
|
+
error=card.error,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def qt_flow_cards_from_entries(entries: tuple[FlowCatalogEntry, ...] | list[FlowCatalogEntry]) -> tuple[QtFlowCard, ...]:
|
|
75
|
+
"""Map discovered catalog entries into shared surface cards."""
|
|
76
|
+
return tuple(qt_flow_card_from_entry(entry) for entry in entries)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_qt_flow_cards(
|
|
80
|
+
flow_catalog_service: "FlowCatalogService",
|
|
81
|
+
*,
|
|
82
|
+
workspace_root: Path | None = None,
|
|
83
|
+
) -> tuple[QtFlowCard, ...]:
|
|
84
|
+
"""Load discovered catalog entries and map them into shared surface cards."""
|
|
85
|
+
return qt_flow_cards_from_entries(flow_catalog_service.load_entries(workspace_root=workspace_root))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"QtFlowCard",
|
|
90
|
+
"default_flow_state",
|
|
91
|
+
"flow_category",
|
|
92
|
+
"flow_catalog_entry_from_qt_card",
|
|
93
|
+
"load_qt_flow_cards",
|
|
94
|
+
"qt_flow_card_from_entry",
|
|
95
|
+
"qt_flow_cards_from_entries",
|
|
96
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Shared presentation helpers across GUI and TUI surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
from data_engine.domain.catalog import FlowCatalogLike
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FlowGroupBucket:
|
|
13
|
+
"""One grouped bucket of flow cards in shared surface order."""
|
|
14
|
+
|
|
15
|
+
group_name: str
|
|
16
|
+
entries: tuple[FlowCatalogLike, ...]
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def title(self) -> str:
|
|
20
|
+
"""Return the user-facing label for this grouped flow section."""
|
|
21
|
+
return group_label(self.group_name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def flow_group_name(card: FlowCatalogLike) -> str:
|
|
25
|
+
"""Return the display/runtime group bucket for one flow card."""
|
|
26
|
+
return card.group or card.mode
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def group_label(group_name: str) -> str:
|
|
30
|
+
"""Return the user-facing label for one grouped flow section."""
|
|
31
|
+
if group_name in {"poll", "schedule", "manual"}:
|
|
32
|
+
return group_name.title()
|
|
33
|
+
return group_name
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def group_cards(cards: tuple[FlowCatalogLike, ...] | list[FlowCatalogLike]) -> tuple[FlowGroupBucket, ...]:
|
|
37
|
+
"""Group cards by display bucket in the shared surface order."""
|
|
38
|
+
grouped: dict[str, list[FlowCatalogLike]] = {}
|
|
39
|
+
for card in cards:
|
|
40
|
+
grouped.setdefault(flow_group_name(card), []).append(card)
|
|
41
|
+
priority = {"manual": 0, "poll": 1, "schedule": 2}
|
|
42
|
+
return tuple(
|
|
43
|
+
FlowGroupBucket(group_name=group_name, entries=tuple(entries))
|
|
44
|
+
for group_name, entries in sorted(grouped.items(), key=lambda item: (priority.get(item[0], 10), item[0].lower()))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def flow_secondary_text(mode: str, state: str) -> str:
|
|
49
|
+
"""Return the secondary status line for one flow card."""
|
|
50
|
+
if mode == "poll":
|
|
51
|
+
return "Polling" if state in {"poll ready", "polling"} else f"Polling {state}"
|
|
52
|
+
if mode == "schedule":
|
|
53
|
+
return "Scheduled" if state in {"schedule ready", "scheduled"} else f"Scheduled {state}"
|
|
54
|
+
return "Manual" if state == "manual" else f"Manual {state}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def group_secondary_text(entries: list[FlowCatalogLike], flow_states: dict[str, str]) -> str:
|
|
58
|
+
"""Return one compact group summary line for sidebar/list displays."""
|
|
59
|
+
total = len(entries)
|
|
60
|
+
active = sum(1 for card in entries if flow_states.get(card.name, card.state) in {"running", "polling", "scheduled"})
|
|
61
|
+
failed = sum(1 for card in entries if flow_states.get(card.name, card.state) == "failed")
|
|
62
|
+
if failed:
|
|
63
|
+
return f"{total} flow(s) Error: {failed}"
|
|
64
|
+
if active:
|
|
65
|
+
return f"{total} flow(s) Running: {active}"
|
|
66
|
+
return f"{total} flow(s)"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def status_color_name(state: str) -> str:
|
|
70
|
+
"""Return the named status color token for one flow state."""
|
|
71
|
+
if state == "failed":
|
|
72
|
+
return "error"
|
|
73
|
+
if state == "started":
|
|
74
|
+
return "started"
|
|
75
|
+
if state in {"running", "polling", "scheduled", "success", "finished"}:
|
|
76
|
+
return "success"
|
|
77
|
+
if state in {"stopping flow", "stopping runtime"}:
|
|
78
|
+
return "warning"
|
|
79
|
+
return "idle"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def state_dot(state: str) -> str:
|
|
83
|
+
"""Return one small textual state marker for compact terminal displays."""
|
|
84
|
+
if state == "failed":
|
|
85
|
+
return "!"
|
|
86
|
+
if state in {"running", "polling", "scheduled", "success", "finished"}:
|
|
87
|
+
return "*"
|
|
88
|
+
if state in {"stopping flow", "stopping runtime"}:
|
|
89
|
+
return "~"
|
|
90
|
+
return "·"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def operation_marker(status: str) -> str:
|
|
94
|
+
"""Return one small textual marker for operation-level progress."""
|
|
95
|
+
if status == "running":
|
|
96
|
+
return ">"
|
|
97
|
+
if status == "success":
|
|
98
|
+
return "+"
|
|
99
|
+
if status == "failed":
|
|
100
|
+
return "!"
|
|
101
|
+
return "·"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_seconds(seconds: float) -> str:
|
|
105
|
+
"""Render elapsed seconds into the compact duration text used across surfaces."""
|
|
106
|
+
|
|
107
|
+
def truncate(value: float, decimals: int = 1) -> float:
|
|
108
|
+
factor = 10**decimals
|
|
109
|
+
return math.trunc(value * factor) / factor
|
|
110
|
+
|
|
111
|
+
if seconds < 0.001:
|
|
112
|
+
return "<1ms"
|
|
113
|
+
if seconds < 1:
|
|
114
|
+
return f"{math.trunc(seconds * 1000)}ms"
|
|
115
|
+
if seconds < 60:
|
|
116
|
+
return f"{truncate(seconds):.1f}s"
|
|
117
|
+
if seconds < 3600:
|
|
118
|
+
return f"{truncate(seconds / 60):.1f}m"
|
|
119
|
+
return f"{truncate(seconds / 3600):.1f}h"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = [
|
|
123
|
+
"FlowGroupBucket",
|
|
124
|
+
"flow_secondary_text",
|
|
125
|
+
"flow_group_name",
|
|
126
|
+
"format_seconds",
|
|
127
|
+
"group_cards",
|
|
128
|
+
"group_label",
|
|
129
|
+
"group_secondary_text",
|
|
130
|
+
"operation_marker",
|
|
131
|
+
"state_dot",
|
|
132
|
+
"status_color_name",
|
|
133
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared run-group presentation helpers across operator surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from data_engine.domain import FlowLogEntry, FlowRunState, RunDetailState
|
|
8
|
+
from data_engine.views.presentation import format_seconds
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class RunGroupDisplay:
|
|
13
|
+
"""Canonical GUI-first presentation state for one grouped run."""
|
|
14
|
+
|
|
15
|
+
primary_label: str
|
|
16
|
+
source_label: str
|
|
17
|
+
status_text: str
|
|
18
|
+
status_visual_state: str
|
|
19
|
+
duration_text: str | None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_run(cls, run_state: FlowRunState) -> "RunGroupDisplay":
|
|
23
|
+
detail = RunDetailState.from_run(run_state)
|
|
24
|
+
return cls(
|
|
25
|
+
primary_label=detail.display_label,
|
|
26
|
+
source_label=detail.source_label,
|
|
27
|
+
status_text=detail.status.title(),
|
|
28
|
+
status_visual_state=_status_visual_state(detail.status),
|
|
29
|
+
duration_text=format_seconds(detail.elapsed_seconds) if detail.elapsed_seconds is not None else None,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_raw_log_message(entry: FlowLogEntry) -> str:
|
|
34
|
+
"""Return canonical user-facing log text for one raw runtime/log entry."""
|
|
35
|
+
from html import escape
|
|
36
|
+
|
|
37
|
+
event = entry.event
|
|
38
|
+
if event is None:
|
|
39
|
+
return escape(entry.line)
|
|
40
|
+
flow_name = escape(event.flow_name)
|
|
41
|
+
source_label = escape(event.source_label)
|
|
42
|
+
status = escape(event.status)
|
|
43
|
+
has_source = event.source_label not in {"", "-"}
|
|
44
|
+
if event.step_name is None:
|
|
45
|
+
if has_source:
|
|
46
|
+
return f"{flow_name} > {source_label} > <i>{status}</i>"
|
|
47
|
+
return f"{flow_name} > <i>{status}</i>"
|
|
48
|
+
step_name = escape(event.step_name.replace(":", "::", 1))
|
|
49
|
+
if has_source:
|
|
50
|
+
return f"{flow_name} > {source_label} > <b>{step_name}</b> - <i>{status}</i>"
|
|
51
|
+
return f"{flow_name} > <b>{step_name}</b> - <i>{status}</i>"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _status_visual_state(status: str) -> str:
|
|
55
|
+
if status in {"failed", "stopped"}:
|
|
56
|
+
return "failed"
|
|
57
|
+
if status == "started":
|
|
58
|
+
return "started"
|
|
59
|
+
return "finished"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["RunGroupDisplay", "format_raw_log_message"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Shared state and presentation helpers for Data Engine operator surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from data_engine.domain.details import FlowSummaryState, OperationArtifactState
|
|
6
|
+
from data_engine.domain.operations import OperationFlowState, OperationRowState, OperationSessionState
|
|
7
|
+
from data_engine.views.models import QtFlowCard
|
|
8
|
+
|
|
9
|
+
def build_flow_summary(card: QtFlowCard | None, flow_states: dict[str, str]) -> FlowSummaryState:
|
|
10
|
+
"""Return summary rows for the selected flow."""
|
|
11
|
+
return FlowSummaryState.from_flow(card, flow_states)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_inspectable_operation(operation_name: str) -> bool:
|
|
15
|
+
"""Return whether an operation can surface a previewable output path."""
|
|
16
|
+
return OperationArtifactState(operation_name).inspectable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def artifact_key_for_operation(operation_name: str) -> str | None:
|
|
20
|
+
"""Return the runtime metadata key produced by one operation."""
|
|
21
|
+
return OperationArtifactState(operation_name).artifact_key
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def capture_step_outputs(flow_card: QtFlowCard, existing: dict[str, "Path"], results: object) -> dict[str, "Path"]:
|
|
25
|
+
"""Return updated output-path mappings extracted from completed flow results."""
|
|
26
|
+
return OperationArtifactState.capture_outputs(flow_card, existing, results)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
OperationDisplayState = OperationFlowState
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"OperationDisplayState",
|
|
34
|
+
"OperationRowState",
|
|
35
|
+
"artifact_key_for_operation",
|
|
36
|
+
"build_flow_summary",
|
|
37
|
+
"capture_step_outputs",
|
|
38
|
+
"is_inspectable_operation",
|
|
39
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Shared surface-facing status-copy helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
WORKSPACE_UNAVAILABLE_TEXT = "Workspace root is no longer available."
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def surface_control_status_text(control_status_text: str | None, *, empty_flow_message: str = "") -> str:
|
|
9
|
+
"""Return the shared control/status line text shown by operator surfaces."""
|
|
10
|
+
return control_status_text or empty_flow_message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["WORKSPACE_UNAVAILABLE_TEXT", "surface_control_status_text"]
|