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
data_engine/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Top-level package for the Data Engine workbook runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from data_engine.authoring.builder import Batch
|
|
9
|
+
from data_engine.authoring.builder import FileRef
|
|
10
|
+
from data_engine.authoring.builder import Flow
|
|
11
|
+
from data_engine.authoring.builder import FlowContext
|
|
12
|
+
from data_engine.authoring.builder import discover_flows, load_flow, run
|
|
13
|
+
|
|
14
|
+
__all__ = ["Batch", "FileRef", "Flow", "FlowContext", "discover_flows", "load_flow", "run"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def __getattr__(name: str):
|
|
18
|
+
"""Lazy-load runtime symbols so lightweight helpers can import package submodules safely."""
|
|
19
|
+
if name in {"Batch", "FileRef", "Flow", "FlowContext", "discover_flows", "load_flow", "run"}:
|
|
20
|
+
from data_engine.authoring.builder import Batch
|
|
21
|
+
from data_engine.authoring.builder import FileRef
|
|
22
|
+
from data_engine.authoring.builder import Flow
|
|
23
|
+
from data_engine.authoring.builder import FlowContext
|
|
24
|
+
from data_engine.authoring.builder import discover_flows
|
|
25
|
+
from data_engine.authoring.builder import load_flow
|
|
26
|
+
from data_engine.authoring.builder import run
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
"Batch": Batch,
|
|
30
|
+
"FileRef": FileRef,
|
|
31
|
+
"Flow": Flow,
|
|
32
|
+
"FlowContext": FlowContext,
|
|
33
|
+
"discover_flows": discover_flows,
|
|
34
|
+
"load_flow": load_flow,
|
|
35
|
+
"run": run,
|
|
36
|
+
}[name]
|
|
37
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Host-agnostic application use cases built on top of services and domain models."""
|
|
2
|
+
|
|
3
|
+
from data_engine.application.actions import ActionStateApplication
|
|
4
|
+
from data_engine.application.catalog import FlowCatalogApplication, FlowCatalogLoadResult, FlowCatalogPresentation
|
|
5
|
+
from data_engine.application.control import FlowRefreshResult, OperatorActionResult, OperatorControlApplication
|
|
6
|
+
from data_engine.application.details import DetailApplication, SelectedFlowPresentation
|
|
7
|
+
from data_engine.application.runtime import (
|
|
8
|
+
DaemonCommandResult,
|
|
9
|
+
EngineRunCompletion,
|
|
10
|
+
FlowStateRefreshPlan,
|
|
11
|
+
ManualRunCompletion,
|
|
12
|
+
RuntimeApplication,
|
|
13
|
+
RuntimeLogMessage,
|
|
14
|
+
RuntimeSnapshotPresentation,
|
|
15
|
+
RuntimeSyncState,
|
|
16
|
+
)
|
|
17
|
+
from data_engine.application.workspace import WorkspaceBinding, WorkspaceSessionApplication
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ActionStateApplication",
|
|
21
|
+
"DaemonCommandResult",
|
|
22
|
+
"DetailApplication",
|
|
23
|
+
"EngineRunCompletion",
|
|
24
|
+
"FlowStateRefreshPlan",
|
|
25
|
+
"FlowRefreshResult",
|
|
26
|
+
"FlowCatalogApplication",
|
|
27
|
+
"FlowCatalogLoadResult",
|
|
28
|
+
"FlowCatalogPresentation",
|
|
29
|
+
"ManualRunCompletion",
|
|
30
|
+
"OperatorActionResult",
|
|
31
|
+
"OperatorControlApplication",
|
|
32
|
+
"RuntimeApplication",
|
|
33
|
+
"RuntimeLogMessage",
|
|
34
|
+
"RuntimeSnapshotPresentation",
|
|
35
|
+
"RuntimeSyncState",
|
|
36
|
+
"SelectedFlowPresentation",
|
|
37
|
+
"WorkspaceSessionApplication",
|
|
38
|
+
"WorkspaceBinding",
|
|
39
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Host-agnostic operator action-state use cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from data_engine.domain import OperatorActionContext, SelectedFlowState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ActionStateApplication:
|
|
9
|
+
"""Own host-neutral action-context assembly for operator surfaces."""
|
|
10
|
+
|
|
11
|
+
def build_action_context(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
card,
|
|
15
|
+
flow_states: dict[str, str],
|
|
16
|
+
runtime_session,
|
|
17
|
+
flow_groups_by_name: dict[str, str | None],
|
|
18
|
+
active_flow_states,
|
|
19
|
+
has_logs: bool,
|
|
20
|
+
has_automated_flows: bool,
|
|
21
|
+
workspace_available: bool = True,
|
|
22
|
+
selected_run_group_present: bool = False,
|
|
23
|
+
) -> OperatorActionContext:
|
|
24
|
+
"""Return one operator action context from current runtime and selection state."""
|
|
25
|
+
selected_flow = SelectedFlowState.from_runtime(
|
|
26
|
+
card=card,
|
|
27
|
+
flow_states=flow_states,
|
|
28
|
+
runtime_session=runtime_session,
|
|
29
|
+
flow_groups_by_name=flow_groups_by_name,
|
|
30
|
+
active_flow_states=active_flow_states,
|
|
31
|
+
has_logs=has_logs,
|
|
32
|
+
)
|
|
33
|
+
return OperatorActionContext(
|
|
34
|
+
runtime_session=runtime_session,
|
|
35
|
+
selected_flow=selected_flow,
|
|
36
|
+
has_automated_flows=has_automated_flows,
|
|
37
|
+
workspace_available=workspace_available,
|
|
38
|
+
selected_run_group_present=selected_run_group_present,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["ActionStateApplication"]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Host-agnostic flow catalog use cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from data_engine.authoring.model import FlowValidationError
|
|
9
|
+
from data_engine.domain import FlowCatalogEntry, FlowCatalogLike, FlowCatalogState
|
|
10
|
+
from data_engine.platform.workspace_models import WorkspacePaths
|
|
11
|
+
from data_engine.services import FlowCatalogService
|
|
12
|
+
from data_engine.views.presentation import group_cards
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class FlowCatalogLoadResult:
|
|
17
|
+
"""Normalized result of one workspace catalog load attempt."""
|
|
18
|
+
|
|
19
|
+
catalog_state: FlowCatalogState
|
|
20
|
+
loaded: bool
|
|
21
|
+
error_text: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class FlowCatalogPresentation:
|
|
26
|
+
"""Normalized grouped catalog presentation shared by operator surfaces."""
|
|
27
|
+
|
|
28
|
+
entries: tuple[FlowCatalogEntry, ...]
|
|
29
|
+
grouped_entries: tuple[tuple[str, tuple[FlowCatalogEntry, ...]], ...]
|
|
30
|
+
selected_flow_name: str | None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def entries_by_name(self) -> dict[str, FlowCatalogEntry]:
|
|
34
|
+
"""Return entries keyed by internal flow name."""
|
|
35
|
+
return {entry.name: entry for entry in self.entries}
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def selected_entry(self) -> FlowCatalogEntry | None:
|
|
39
|
+
"""Return the normalized selected entry, if any."""
|
|
40
|
+
if self.selected_flow_name is None:
|
|
41
|
+
return None
|
|
42
|
+
return self.entries_by_name.get(self.selected_flow_name)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def cards(self) -> tuple[FlowCatalogLike, ...]:
|
|
46
|
+
"""Return catalog entries under the shared flow metadata protocol."""
|
|
47
|
+
return self.entries
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def grouped_cards(self) -> tuple[tuple[str, tuple[FlowCatalogLike, ...]], ...]:
|
|
51
|
+
"""Return grouped entries under the shared flow metadata protocol."""
|
|
52
|
+
return self.grouped_entries
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def selected_card(self) -> FlowCatalogLike | None:
|
|
56
|
+
"""Return the selected flow metadata under the shared flow protocol."""
|
|
57
|
+
return self.selected_entry
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def selected_list_index(self) -> int | None:
|
|
61
|
+
"""Return the list index for the selected flow in a grouped header+item list."""
|
|
62
|
+
if self.selected_flow_name is None:
|
|
63
|
+
return None
|
|
64
|
+
index = 0
|
|
65
|
+
for _group_name, entries in self.grouped_entries:
|
|
66
|
+
index += 1
|
|
67
|
+
for entry in entries:
|
|
68
|
+
if entry.name == self.selected_flow_name:
|
|
69
|
+
return index
|
|
70
|
+
index += 1
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FlowCatalogApplication:
|
|
75
|
+
"""Own host-neutral flow catalog loading and state transitions."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, *, flow_catalog_service: FlowCatalogService) -> None:
|
|
78
|
+
self.flow_catalog_service = flow_catalog_service
|
|
79
|
+
|
|
80
|
+
def load_state(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
workspace_root: Path,
|
|
84
|
+
current_state: FlowCatalogState | None = None,
|
|
85
|
+
) -> FlowCatalogState:
|
|
86
|
+
"""Load discovered entries and merge them into one catalog state."""
|
|
87
|
+
base = current_state or FlowCatalogState.empty()
|
|
88
|
+
entries = self.flow_catalog_service.load_entries(workspace_root=workspace_root)
|
|
89
|
+
return base.with_entries(entries).with_empty_message("")
|
|
90
|
+
|
|
91
|
+
def empty_state(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
message: str = "",
|
|
95
|
+
current_state: FlowCatalogState | None = None,
|
|
96
|
+
) -> FlowCatalogState:
|
|
97
|
+
"""Return an empty catalog state with one host-provided message."""
|
|
98
|
+
base = current_state or FlowCatalogState.empty()
|
|
99
|
+
return FlowCatalogState.empty(empty_message=message).with_selected_flow_name(base.selected_flow_name)
|
|
100
|
+
|
|
101
|
+
def select_flow(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
catalog_state: FlowCatalogState,
|
|
105
|
+
flow_name: str | None,
|
|
106
|
+
) -> FlowCatalogState:
|
|
107
|
+
"""Return catalog state with one normalized selected flow."""
|
|
108
|
+
return catalog_state.with_selected_flow_name(flow_name)
|
|
109
|
+
|
|
110
|
+
def build_presentation(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
catalog_state: FlowCatalogState,
|
|
114
|
+
) -> FlowCatalogPresentation:
|
|
115
|
+
"""Return grouped UI-friendly catalog presentation from one catalog state."""
|
|
116
|
+
grouped = tuple(
|
|
117
|
+
(bucket.group_name, bucket.entries)
|
|
118
|
+
for bucket in group_cards(catalog_state.entries)
|
|
119
|
+
)
|
|
120
|
+
return FlowCatalogPresentation(
|
|
121
|
+
entries=catalog_state.entries,
|
|
122
|
+
grouped_entries=grouped,
|
|
123
|
+
selected_flow_name=catalog_state.selected_flow_name,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def load_workspace_catalog(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
workspace_paths: WorkspacePaths,
|
|
130
|
+
current_state: FlowCatalogState | None = None,
|
|
131
|
+
missing_message: str = "No flow modules discovered.",
|
|
132
|
+
) -> FlowCatalogLoadResult:
|
|
133
|
+
"""Return one normalized catalog load result for a resolved workspace binding."""
|
|
134
|
+
if not workspace_paths.flow_modules_dir.is_dir():
|
|
135
|
+
return FlowCatalogLoadResult(
|
|
136
|
+
catalog_state=self.empty_state(message=missing_message, current_state=current_state),
|
|
137
|
+
loaded=False,
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
catalog_state = self.load_state(
|
|
141
|
+
workspace_root=workspace_paths.workspace_root,
|
|
142
|
+
current_state=current_state,
|
|
143
|
+
)
|
|
144
|
+
except FlowValidationError as exc:
|
|
145
|
+
message = str(exc)
|
|
146
|
+
return FlowCatalogLoadResult(
|
|
147
|
+
catalog_state=self.empty_state(message=message, current_state=current_state),
|
|
148
|
+
loaded=False,
|
|
149
|
+
error_text=message,
|
|
150
|
+
)
|
|
151
|
+
return FlowCatalogLoadResult(catalog_state=catalog_state, loaded=True)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Host-agnostic operator control and action use cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from data_engine.domain import RuntimeSessionState
|
|
8
|
+
from data_engine.hosts.daemon.manager import WorkspaceDaemonManager
|
|
9
|
+
from data_engine.platform.workspace_models import WorkspacePaths, authored_workspace_is_available
|
|
10
|
+
from data_engine.services import DaemonStateService
|
|
11
|
+
|
|
12
|
+
from data_engine.application.runtime import RuntimeApplication
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class OperatorActionResult:
|
|
17
|
+
"""Normalized result for one operator control action."""
|
|
18
|
+
|
|
19
|
+
requested: bool
|
|
20
|
+
sync_after: bool = False
|
|
21
|
+
ensure_daemon_started: bool = False
|
|
22
|
+
status_text: str | None = None
|
|
23
|
+
error_text: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FlowRefreshResult:
|
|
28
|
+
"""Normalized result for one flow-refresh request."""
|
|
29
|
+
|
|
30
|
+
reload_catalog: bool
|
|
31
|
+
sync_after: bool = False
|
|
32
|
+
status_text: str | None = None
|
|
33
|
+
warning_text: str | None = None
|
|
34
|
+
error_text: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OperatorControlApplication:
|
|
38
|
+
"""Own host-neutral action orchestration for operator surfaces."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
runtime_application: RuntimeApplication,
|
|
44
|
+
daemon_state_service: DaemonStateService,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.runtime_application = runtime_application
|
|
47
|
+
self.daemon_state_service = daemon_state_service
|
|
48
|
+
|
|
49
|
+
def run_selected_flow(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
paths: WorkspacePaths,
|
|
53
|
+
runtime_session: RuntimeSessionState,
|
|
54
|
+
selected_flow_name: str | None,
|
|
55
|
+
selected_flow_valid: bool,
|
|
56
|
+
selected_flow_group: str | None,
|
|
57
|
+
selected_flow_group_active: bool,
|
|
58
|
+
blocked_status_text: str,
|
|
59
|
+
timeout: float = 2.0,
|
|
60
|
+
) -> OperatorActionResult:
|
|
61
|
+
"""Validate and request one manual run for the selected flow."""
|
|
62
|
+
if not authored_workspace_is_available(paths):
|
|
63
|
+
return OperatorActionResult(requested=False, error_text="Workspace root is no longer available.")
|
|
64
|
+
if selected_flow_name is None:
|
|
65
|
+
return OperatorActionResult(requested=False, status_text="Select one flow first.")
|
|
66
|
+
if not selected_flow_valid:
|
|
67
|
+
return OperatorActionResult(
|
|
68
|
+
requested=False,
|
|
69
|
+
status_text=f"{selected_flow_name} is invalid and cannot run.",
|
|
70
|
+
)
|
|
71
|
+
if selected_flow_group_active or runtime_session.runtime_active or runtime_session.manual_run_active:
|
|
72
|
+
return OperatorActionResult(requested=False)
|
|
73
|
+
if not runtime_session.control_available:
|
|
74
|
+
return OperatorActionResult(requested=False, status_text=blocked_status_text)
|
|
75
|
+
result = self.runtime_application.run_flow(
|
|
76
|
+
paths,
|
|
77
|
+
name=selected_flow_name,
|
|
78
|
+
wait=False,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
if not result.ok:
|
|
82
|
+
return OperatorActionResult(
|
|
83
|
+
requested=False,
|
|
84
|
+
error_text=_verbose_action_error(
|
|
85
|
+
f"run {selected_flow_name}",
|
|
86
|
+
result.error,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
return OperatorActionResult(
|
|
90
|
+
requested=True,
|
|
91
|
+
sync_after=True,
|
|
92
|
+
status_text=f"Running {selected_flow_name}...",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def start_engine(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
paths: WorkspacePaths,
|
|
99
|
+
runtime_session: RuntimeSessionState,
|
|
100
|
+
has_automated_flows: bool,
|
|
101
|
+
blocked_status_text: str,
|
|
102
|
+
timeout: float = 2.0,
|
|
103
|
+
) -> OperatorActionResult:
|
|
104
|
+
"""Validate and request automated engine start."""
|
|
105
|
+
if not authored_workspace_is_available(paths):
|
|
106
|
+
return OperatorActionResult(requested=False, error_text="Workspace root is no longer available.")
|
|
107
|
+
if runtime_session.runtime_active or runtime_session.runtime_stopping or runtime_session.manual_run_active:
|
|
108
|
+
return OperatorActionResult(requested=False)
|
|
109
|
+
if not runtime_session.control_available:
|
|
110
|
+
return OperatorActionResult(requested=False, status_text=blocked_status_text)
|
|
111
|
+
if not has_automated_flows:
|
|
112
|
+
return OperatorActionResult(requested=False, status_text="No automated flows are available.")
|
|
113
|
+
result = self.runtime_application.start_engine(paths, timeout=timeout)
|
|
114
|
+
if not result.ok:
|
|
115
|
+
return OperatorActionResult(
|
|
116
|
+
requested=False,
|
|
117
|
+
error_text=_verbose_action_error("start the automated engine", result.error),
|
|
118
|
+
)
|
|
119
|
+
return OperatorActionResult(requested=True, sync_after=True, status_text="Starting automated engine...")
|
|
120
|
+
|
|
121
|
+
def stop_pipeline(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
paths: WorkspacePaths,
|
|
125
|
+
runtime_session: RuntimeSessionState,
|
|
126
|
+
selected_flow_group: str | None,
|
|
127
|
+
blocked_status_text: str,
|
|
128
|
+
timeout: float = 2.0,
|
|
129
|
+
) -> OperatorActionResult:
|
|
130
|
+
"""Validate and request stop for the engine or selected manual flow."""
|
|
131
|
+
if runtime_session.runtime_active:
|
|
132
|
+
result = self.runtime_application.stop_engine(paths, timeout=timeout)
|
|
133
|
+
if not result.ok:
|
|
134
|
+
return OperatorActionResult(
|
|
135
|
+
requested=False,
|
|
136
|
+
error_text=_verbose_action_error("stop the engine", result.error),
|
|
137
|
+
)
|
|
138
|
+
return OperatorActionResult(requested=True, sync_after=True, status_text="Stopping engine...")
|
|
139
|
+
if runtime_session.manual_run_active:
|
|
140
|
+
if not runtime_session.control_available:
|
|
141
|
+
return OperatorActionResult(requested=False, status_text=blocked_status_text)
|
|
142
|
+
flow_name = runtime_session.active_manual_runs.get(selected_flow_group)
|
|
143
|
+
if flow_name is None:
|
|
144
|
+
return OperatorActionResult(requested=False)
|
|
145
|
+
result = self.runtime_application.stop_flow(paths, name=flow_name, timeout=timeout)
|
|
146
|
+
if not result.ok:
|
|
147
|
+
return OperatorActionResult(
|
|
148
|
+
requested=False,
|
|
149
|
+
error_text=_verbose_action_error(f"stop {flow_name}", result.error),
|
|
150
|
+
)
|
|
151
|
+
return OperatorActionResult(requested=True, sync_after=True, status_text="Stopping selected flow...")
|
|
152
|
+
return OperatorActionResult(requested=False)
|
|
153
|
+
|
|
154
|
+
def request_control(self, daemon_manager: WorkspaceDaemonManager) -> OperatorActionResult:
|
|
155
|
+
"""Request workspace control through the daemon-state manager."""
|
|
156
|
+
try:
|
|
157
|
+
message = self.daemon_state_service.request_control(daemon_manager)
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
return OperatorActionResult(
|
|
160
|
+
requested=False,
|
|
161
|
+
error_text=_verbose_action_error("request workspace control", exc),
|
|
162
|
+
)
|
|
163
|
+
return OperatorActionResult(
|
|
164
|
+
requested=True,
|
|
165
|
+
sync_after=True,
|
|
166
|
+
ensure_daemon_started=True,
|
|
167
|
+
status_text=message,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def refresh_flows(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
paths: WorkspacePaths,
|
|
174
|
+
runtime_session: RuntimeSessionState,
|
|
175
|
+
has_authored_workspace: bool,
|
|
176
|
+
timeout: float = 5.0,
|
|
177
|
+
) -> FlowRefreshResult:
|
|
178
|
+
"""Validate and request one flow refresh while preserving local reload behavior."""
|
|
179
|
+
if runtime_session.runtime_active or runtime_session.active_manual_runs:
|
|
180
|
+
return FlowRefreshResult(
|
|
181
|
+
reload_catalog=False,
|
|
182
|
+
error_text="Stop active engine or manual runs before refreshing flows.",
|
|
183
|
+
)
|
|
184
|
+
if not has_authored_workspace:
|
|
185
|
+
return FlowRefreshResult(
|
|
186
|
+
reload_catalog=True,
|
|
187
|
+
sync_after=True,
|
|
188
|
+
status_text="No flow modules discovered.",
|
|
189
|
+
)
|
|
190
|
+
result = self.runtime_application.refresh_flows(paths, timeout=timeout)
|
|
191
|
+
if not result.ok:
|
|
192
|
+
return FlowRefreshResult(
|
|
193
|
+
reload_catalog=True,
|
|
194
|
+
sync_after=True,
|
|
195
|
+
status_text="Reloaded flow definitions.",
|
|
196
|
+
warning_text=_verbose_action_error("refresh flows", result.error),
|
|
197
|
+
)
|
|
198
|
+
return FlowRefreshResult(
|
|
199
|
+
reload_catalog=True,
|
|
200
|
+
sync_after=True,
|
|
201
|
+
status_text="Reloaded flow definitions.",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _verbose_action_error(action: str, detail: object | None) -> str:
|
|
206
|
+
"""Return a non-terse user-facing failure string for operator control paths."""
|
|
207
|
+
text = str(detail).strip() if detail is not None else ""
|
|
208
|
+
if text:
|
|
209
|
+
return text
|
|
210
|
+
return f"Failed to {action}. The daemon returned no additional detail."
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = ["FlowRefreshResult", "OperatorActionResult", "OperatorControlApplication"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Host-agnostic selected-flow and run-detail use cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from data_engine.domain import FlowCatalogLike, FlowRunState, SelectedFlowDetailState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class SelectedFlowPresentation:
|
|
12
|
+
"""Normalized selected-flow detail state for operator surfaces."""
|
|
13
|
+
|
|
14
|
+
detail_state: SelectedFlowDetailState | None
|
|
15
|
+
run_groups: tuple[FlowRunState, ...]
|
|
16
|
+
visible_run_groups: tuple[FlowRunState, ...]
|
|
17
|
+
selected_run_key: tuple[str, str] | None
|
|
18
|
+
empty_text: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def run_group_signature(self) -> tuple[tuple[str, str], ...]:
|
|
22
|
+
"""Return the stable visible run-list signature for diffing/render reuse."""
|
|
23
|
+
return tuple(group.key for group in self.visible_run_groups)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def selected_run_group(self) -> FlowRunState | None:
|
|
27
|
+
"""Return the normalized selected run group, if any."""
|
|
28
|
+
if self.selected_run_key is not None:
|
|
29
|
+
for run_group in self.run_groups:
|
|
30
|
+
if run_group.key == self.selected_run_key:
|
|
31
|
+
return run_group
|
|
32
|
+
return self.run_groups[0] if self.run_groups else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DetailApplication:
|
|
36
|
+
"""Own host-neutral selected-flow detail and run selection behavior."""
|
|
37
|
+
|
|
38
|
+
def build_selected_flow_presentation(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
card: FlowCatalogLike | None,
|
|
42
|
+
tracker,
|
|
43
|
+
flow_states: dict[str, str],
|
|
44
|
+
run_groups: tuple[FlowRunState, ...],
|
|
45
|
+
selected_run_key: tuple[str, str] | None,
|
|
46
|
+
max_visible_runs: int | None = None,
|
|
47
|
+
) -> SelectedFlowPresentation:
|
|
48
|
+
"""Return the selected-flow detail state and normalized run selection."""
|
|
49
|
+
if card is None:
|
|
50
|
+
return SelectedFlowPresentation(
|
|
51
|
+
detail_state=None,
|
|
52
|
+
run_groups=(),
|
|
53
|
+
visible_run_groups=(),
|
|
54
|
+
selected_run_key=None,
|
|
55
|
+
empty_text="Select one flow to see details.",
|
|
56
|
+
)
|
|
57
|
+
detail_state = SelectedFlowDetailState.from_flow(
|
|
58
|
+
card,
|
|
59
|
+
tracker,
|
|
60
|
+
flow_states=flow_states,
|
|
61
|
+
)
|
|
62
|
+
normalized_key = selected_run_key if any(group.key == selected_run_key for group in run_groups) else (run_groups[0].key if run_groups else None)
|
|
63
|
+
visible_run_groups = run_groups[-max_visible_runs:] if max_visible_runs is not None and max_visible_runs >= 0 else run_groups
|
|
64
|
+
return SelectedFlowPresentation(
|
|
65
|
+
detail_state=detail_state,
|
|
66
|
+
run_groups=run_groups,
|
|
67
|
+
visible_run_groups=visible_run_groups,
|
|
68
|
+
selected_run_key=normalized_key,
|
|
69
|
+
empty_text="",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["DetailApplication", "SelectedFlowPresentation"]
|