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,210 @@
|
|
|
1
|
+
"""Single flow-run execution lifecycle for authored flows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from time import monotonic
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from data_engine.authoring.model import FlowExecutionError, FlowStoppedError, FlowValidationError
|
|
10
|
+
from data_engine.authoring.primitives import FlowContext, WatchSpec
|
|
11
|
+
from data_engine.domain.time import utcnow_text
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from data_engine.authoring.execution.single import _FlowRuntime
|
|
15
|
+
from data_engine.authoring.flow import Flow
|
|
16
|
+
from data_engine.authoring.primitives import StepSpec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlowRunExecutor:
|
|
20
|
+
"""Own one-run lifecycle, step execution, and ledger/log updates."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, runtime: "_FlowRuntime") -> None:
|
|
23
|
+
self.runtime = runtime
|
|
24
|
+
|
|
25
|
+
def run_one(self, flow: "Flow", source_path: "Path | None", *, batch_signatures=()) -> FlowContext:
|
|
26
|
+
self.runtime._check_flow_stop()
|
|
27
|
+
run_id = self.runtime.context_builder.new_run_id()
|
|
28
|
+
context = self.runtime.context_builder.build(flow, source_path, run_id=run_id)
|
|
29
|
+
run_started = monotonic()
|
|
30
|
+
signature = self.runtime.polling.poll_source_signature(flow, source_path)
|
|
31
|
+
effective_signatures = batch_signatures or ((signature,) if signature is not None else ())
|
|
32
|
+
started_at_utc = str(context.metadata["started_at_utc"])
|
|
33
|
+
normalized_source_path = signature.source_path if signature is not None else self.runtime.polling.normalized_source_path(source_path)
|
|
34
|
+
self.runtime.runtime_ledger.record_run_started(
|
|
35
|
+
run_id=run_id,
|
|
36
|
+
flow_name=context.flow_name,
|
|
37
|
+
group_name=context.group,
|
|
38
|
+
source_path=normalized_source_path,
|
|
39
|
+
started_at_utc=started_at_utc,
|
|
40
|
+
)
|
|
41
|
+
for effective_signature in effective_signatures:
|
|
42
|
+
self.runtime.runtime_ledger.upsert_file_state(flow_name=context.flow_name, signature=effective_signature, status="started")
|
|
43
|
+
self.runtime.log_emitter.log_flow_event(run_id, context.flow_name, source_path, status="started")
|
|
44
|
+
try:
|
|
45
|
+
self._ensure_runtime_sources_available(flow, context, source_path)
|
|
46
|
+
for step in flow.steps:
|
|
47
|
+
self.runtime._check_flow_stop()
|
|
48
|
+
self._load_current_for_step(context, step)
|
|
49
|
+
step_started = monotonic()
|
|
50
|
+
step_started_at_utc = utcnow_text()
|
|
51
|
+
step_run_id = self.runtime.runtime_ledger.record_step_started(
|
|
52
|
+
run_id=run_id,
|
|
53
|
+
flow_name=context.flow_name,
|
|
54
|
+
step_label=step.label,
|
|
55
|
+
started_at_utc=step_started_at_utc,
|
|
56
|
+
)
|
|
57
|
+
self.runtime.log_emitter.log_step_event(run_id, context.flow_name, step.label, source_path, status="started")
|
|
58
|
+
try:
|
|
59
|
+
result = step.fn(context)
|
|
60
|
+
except FlowStoppedError:
|
|
61
|
+
raise
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
failure = FlowExecutionError(
|
|
64
|
+
flow_name=context.flow_name,
|
|
65
|
+
phase="step",
|
|
66
|
+
step_label=step.label,
|
|
67
|
+
function_name=step.function_name,
|
|
68
|
+
source_path=source_path,
|
|
69
|
+
detail=f"{type(exc).__name__}: {exc}",
|
|
70
|
+
)
|
|
71
|
+
elapsed_ms = max(int((monotonic() - step_started) * 1000), 0)
|
|
72
|
+
self.runtime.runtime_ledger.record_step_finished(
|
|
73
|
+
step_run_id=step_run_id,
|
|
74
|
+
status="failed",
|
|
75
|
+
finished_at_utc=utcnow_text(),
|
|
76
|
+
elapsed_ms=elapsed_ms,
|
|
77
|
+
error_text=str(failure),
|
|
78
|
+
)
|
|
79
|
+
self.runtime.log_emitter.log_runtime_message(
|
|
80
|
+
str(failure),
|
|
81
|
+
level="error",
|
|
82
|
+
run_id=run_id,
|
|
83
|
+
flow_name=context.flow_name,
|
|
84
|
+
step_label=step.label,
|
|
85
|
+
)
|
|
86
|
+
raise failure from exc
|
|
87
|
+
context.current = result
|
|
88
|
+
if step.save_as is not None:
|
|
89
|
+
context.objects[step.save_as] = result
|
|
90
|
+
if isinstance(result, Path) and result.exists():
|
|
91
|
+
step_outputs = context.metadata.setdefault("step_outputs", {})
|
|
92
|
+
if isinstance(step_outputs, dict):
|
|
93
|
+
step_outputs[step.label] = result
|
|
94
|
+
elapsed = monotonic() - step_started
|
|
95
|
+
elapsed_ms = max(int(elapsed * 1000), 0)
|
|
96
|
+
self.runtime.runtime_ledger.record_step_finished(
|
|
97
|
+
step_run_id=step_run_id,
|
|
98
|
+
status="success",
|
|
99
|
+
finished_at_utc=utcnow_text(),
|
|
100
|
+
elapsed_ms=elapsed_ms,
|
|
101
|
+
output_path=str(result.resolve()) if isinstance(result, Path) and result.exists() else None,
|
|
102
|
+
)
|
|
103
|
+
self.runtime.log_emitter.log_step_event(run_id, context.flow_name, step.label, source_path, status="success", elapsed=elapsed)
|
|
104
|
+
except FlowStoppedError as exc:
|
|
105
|
+
finished_at_utc = utcnow_text()
|
|
106
|
+
self.runtime.runtime_ledger.record_run_finished(run_id=run_id, status="stopped", finished_at_utc=finished_at_utc, error_text=str(exc))
|
|
107
|
+
for effective_signature in effective_signatures:
|
|
108
|
+
self.runtime.runtime_ledger.upsert_file_state(
|
|
109
|
+
flow_name=context.flow_name,
|
|
110
|
+
signature=effective_signature,
|
|
111
|
+
status="stopped",
|
|
112
|
+
error_text=str(exc),
|
|
113
|
+
)
|
|
114
|
+
self.runtime.log_emitter.log_flow_event(run_id, context.flow_name, source_path, status="stopped", elapsed=monotonic() - run_started)
|
|
115
|
+
raise
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
elapsed = monotonic() - run_started
|
|
118
|
+
finished_at_utc = utcnow_text()
|
|
119
|
+
failed_step = step.label if "step" in locals() else None
|
|
120
|
+
failure_text = str(exc)
|
|
121
|
+
self.runtime.runtime_ledger.record_run_finished(run_id=run_id, status="failed", finished_at_utc=finished_at_utc, error_text=failure_text)
|
|
122
|
+
for effective_signature in effective_signatures:
|
|
123
|
+
self.runtime.runtime_ledger.upsert_file_state(
|
|
124
|
+
flow_name=context.flow_name,
|
|
125
|
+
signature=effective_signature,
|
|
126
|
+
status="failed",
|
|
127
|
+
error_text=failure_text,
|
|
128
|
+
)
|
|
129
|
+
if failed_step is None:
|
|
130
|
+
self.runtime.log_emitter.log_runtime_message(failure_text, level="error", run_id=run_id, flow_name=context.flow_name)
|
|
131
|
+
self.runtime.log_emitter.log_flow_event(run_id, context.flow_name, source_path, status="failed", elapsed=elapsed, level="error", exc_info=True)
|
|
132
|
+
else:
|
|
133
|
+
self.runtime.log_emitter.log_step_event(
|
|
134
|
+
run_id,
|
|
135
|
+
context.flow_name,
|
|
136
|
+
failed_step,
|
|
137
|
+
source_path,
|
|
138
|
+
status="failed",
|
|
139
|
+
elapsed=elapsed,
|
|
140
|
+
level="error",
|
|
141
|
+
exc_info=True,
|
|
142
|
+
)
|
|
143
|
+
self.runtime.log_emitter.log_flow_event(run_id, context.flow_name, source_path, status="failed", elapsed=elapsed)
|
|
144
|
+
raise
|
|
145
|
+
total = monotonic() - run_started
|
|
146
|
+
finished_at_utc = utcnow_text()
|
|
147
|
+
self.runtime.runtime_ledger.record_run_finished(run_id=run_id, status="success", finished_at_utc=finished_at_utc)
|
|
148
|
+
for effective_signature in effective_signatures:
|
|
149
|
+
self.runtime.runtime_ledger.upsert_file_state(
|
|
150
|
+
flow_name=context.flow_name,
|
|
151
|
+
signature=effective_signature,
|
|
152
|
+
status="success",
|
|
153
|
+
run_id=run_id,
|
|
154
|
+
finished_at_utc=finished_at_utc,
|
|
155
|
+
)
|
|
156
|
+
self.runtime.log_emitter.log_flow_event(run_id, context.flow_name, source_path, status="success", elapsed=total)
|
|
157
|
+
return context
|
|
158
|
+
|
|
159
|
+
def preview_one(self, flow: "Flow", source_path: "Path | None", *, use: str | None) -> FlowContext:
|
|
160
|
+
self.runtime._check_flow_stop()
|
|
161
|
+
context = self.runtime.context_builder.build(flow, source_path, run_id="preview")
|
|
162
|
+
self._ensure_runtime_sources_available(flow, context, source_path)
|
|
163
|
+
for step in flow.steps:
|
|
164
|
+
self.runtime._check_flow_stop()
|
|
165
|
+
self._load_current_for_step(context, step)
|
|
166
|
+
try:
|
|
167
|
+
result = step.fn(context)
|
|
168
|
+
except FlowStoppedError:
|
|
169
|
+
raise
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
raise FlowExecutionError(
|
|
172
|
+
flow_name=context.flow_name,
|
|
173
|
+
phase="step",
|
|
174
|
+
step_label=step.label,
|
|
175
|
+
function_name=step.function_name,
|
|
176
|
+
source_path=source_path,
|
|
177
|
+
detail=f"{type(exc).__name__}: {exc}",
|
|
178
|
+
) from exc
|
|
179
|
+
context.current = result
|
|
180
|
+
if step.save_as is not None:
|
|
181
|
+
context.objects[step.save_as] = result
|
|
182
|
+
if use is not None and step.save_as == use:
|
|
183
|
+
context.current = result
|
|
184
|
+
return context
|
|
185
|
+
return context
|
|
186
|
+
|
|
187
|
+
def _ensure_runtime_sources_available(self, flow: "Flow", context: FlowContext, source_path: "Path | None") -> None:
|
|
188
|
+
trigger = flow.trigger
|
|
189
|
+
if not isinstance(trigger, WatchSpec) or trigger.source is None:
|
|
190
|
+
return
|
|
191
|
+
if not trigger.source.exists():
|
|
192
|
+
raise FlowValidationError(f"Source path not found: {trigger.source}")
|
|
193
|
+
if trigger.source.is_file():
|
|
194
|
+
source_path = context.source.path if context.source is not None else source_path
|
|
195
|
+
if source_path is None or not source_path.exists():
|
|
196
|
+
raise FlowValidationError(f"Source file not found: {trigger.source}")
|
|
197
|
+
if not source_path.is_file():
|
|
198
|
+
raise FlowValidationError(f"Source file is not a file: {trigger.source}")
|
|
199
|
+
elif not trigger.source.is_dir():
|
|
200
|
+
raise FlowValidationError(f"Source path is neither a file nor a directory: {trigger.source}")
|
|
201
|
+
|
|
202
|
+
def _load_current_for_step(self, context: FlowContext, step: "StepSpec") -> None:
|
|
203
|
+
if step.use is None or step.use == "current":
|
|
204
|
+
return
|
|
205
|
+
if step.use not in context.objects:
|
|
206
|
+
raise FlowValidationError(f"Step {step.label!r} requested missing object {step.use!r}.")
|
|
207
|
+
context.current = context.objects[step.use]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
__all__ = ["FlowRunExecutor"]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Single-runtime orchestration for authored flows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import threading
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Callable
|
|
9
|
+
|
|
10
|
+
from data_engine.authoring.model import FlowStoppedError, FlowValidationError
|
|
11
|
+
from data_engine.authoring.primitives import FlowContext, WatchSpec
|
|
12
|
+
from data_engine.authoring.execution.continuous import ContinuousRuntimeLoop
|
|
13
|
+
from data_engine.authoring.execution.context import RuntimeContextBuilder
|
|
14
|
+
from data_engine.authoring.execution.logging import RuntimeLogEmitter
|
|
15
|
+
from data_engine.authoring.execution.polling import RuntimePollingSupport
|
|
16
|
+
from data_engine.authoring.execution.runner import FlowRunExecutor
|
|
17
|
+
from data_engine.runtime.file_watch import PollingWatcher
|
|
18
|
+
from data_engine.runtime.runtime_db import RuntimeLedger
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from data_engine.authoring.flow import Flow
|
|
22
|
+
from data_engine.authoring.primitives import StepSpec
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _open_default_runtime_ledger() -> RuntimeLedger:
|
|
26
|
+
"""Open the default runtime ledger for authored flow execution."""
|
|
27
|
+
return RuntimeLedger.open_default()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class RuntimeLedgerService:
|
|
32
|
+
"""Own how authored flow execution opens its runtime ledger."""
|
|
33
|
+
|
|
34
|
+
open_runtime_ledger_func: Callable[[], RuntimeLedger]
|
|
35
|
+
|
|
36
|
+
def open_runtime_ledger(self) -> RuntimeLedger:
|
|
37
|
+
"""Open one runtime ledger for authored flow execution."""
|
|
38
|
+
return self.open_runtime_ledger_func()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_runtime_ledger_service() -> RuntimeLedgerService:
|
|
42
|
+
"""Build the default runtime-ledger service for authored flows."""
|
|
43
|
+
return RuntimeLedgerService(open_runtime_ledger_func=_open_default_runtime_ledger)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _FlowRuntime:
|
|
47
|
+
"""Sequential runtime that executes one or more configured flows."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
flows: tuple["Flow", ...],
|
|
52
|
+
*,
|
|
53
|
+
continuous: bool,
|
|
54
|
+
runtime_stop_event: threading.Event | None = None,
|
|
55
|
+
flow_stop_event: threading.Event | None = None,
|
|
56
|
+
status_callback: Callable[[str], None] | None = None,
|
|
57
|
+
runtime_ledger: RuntimeLedger | None = None,
|
|
58
|
+
runtime_ledger_service: RuntimeLedgerService | None = None,
|
|
59
|
+
runtime_ledger_factory: Callable[[], RuntimeLedger] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.flows = tuple(flows)
|
|
62
|
+
self.continuous = continuous
|
|
63
|
+
self.runtime_stop_event = runtime_stop_event
|
|
64
|
+
self.flow_stop_event = flow_stop_event
|
|
65
|
+
self.status_callback = status_callback
|
|
66
|
+
runtime_ledger_service = runtime_ledger_service or default_runtime_ledger_service()
|
|
67
|
+
self._runtime_ledger_factory = runtime_ledger_factory or runtime_ledger_service.open_runtime_ledger
|
|
68
|
+
self._owns_runtime_ledger = runtime_ledger is None
|
|
69
|
+
self.runtime_ledger = runtime_ledger or self._runtime_ledger_factory()
|
|
70
|
+
self.context_builder = RuntimeContextBuilder()
|
|
71
|
+
self.log_emitter = RuntimeLogEmitter(self.runtime_ledger)
|
|
72
|
+
self.polling = RuntimePollingSupport(self.runtime_ledger)
|
|
73
|
+
self.run_executor = FlowRunExecutor(self)
|
|
74
|
+
self.continuous_loop = ContinuousRuntimeLoop(self)
|
|
75
|
+
|
|
76
|
+
def run(self) -> list[FlowContext]:
|
|
77
|
+
try:
|
|
78
|
+
self._validate()
|
|
79
|
+
if not self.continuous or all(flow.mode == "manual" for flow in self.flows):
|
|
80
|
+
return self._run_once_all()
|
|
81
|
+
return self.continuous_loop.run()
|
|
82
|
+
finally:
|
|
83
|
+
self._close_owned_runtime_ledger()
|
|
84
|
+
|
|
85
|
+
def preview(self, *, use: str | None = None):
|
|
86
|
+
"""Run exactly one flow for notebook-style inspection and return one object."""
|
|
87
|
+
try:
|
|
88
|
+
self._validate()
|
|
89
|
+
if len(self.flows) != 1:
|
|
90
|
+
raise FlowValidationError("preview() requires exactly one flow.")
|
|
91
|
+
flow = self.flows[0]
|
|
92
|
+
startup_sources = self.polling.startup_sources(flow)
|
|
93
|
+
if not startup_sources:
|
|
94
|
+
raise FlowValidationError("preview() could not determine a startup source.")
|
|
95
|
+
context = self.run_executor.preview_one(flow, startup_sources[0], use=use)
|
|
96
|
+
if use is None or use == "current":
|
|
97
|
+
return context.current
|
|
98
|
+
if use not in context.objects:
|
|
99
|
+
raise FlowValidationError(f"preview() could not find saved object {use!r}.")
|
|
100
|
+
return context.objects[use]
|
|
101
|
+
finally:
|
|
102
|
+
self._close_owned_runtime_ledger()
|
|
103
|
+
|
|
104
|
+
def _close_owned_runtime_ledger(self) -> None:
|
|
105
|
+
"""Close the runtime ledger when this runtime opened it implicitly."""
|
|
106
|
+
if not self._owns_runtime_ledger:
|
|
107
|
+
return
|
|
108
|
+
self.runtime_ledger.close()
|
|
109
|
+
|
|
110
|
+
def _validate(self) -> None:
|
|
111
|
+
names = [flow.name for flow in self.flows]
|
|
112
|
+
if any(name is None or not str(name).strip() for name in names):
|
|
113
|
+
raise FlowValidationError("Flow names must be set before execution.")
|
|
114
|
+
if len(set(names)) != len(names):
|
|
115
|
+
raise FlowValidationError("Flow names must be unique within one runtime.")
|
|
116
|
+
for flow in self.flows:
|
|
117
|
+
if not flow.steps:
|
|
118
|
+
raise FlowValidationError(f"Flow {flow.name!r} must define at least one step.")
|
|
119
|
+
|
|
120
|
+
def _run_once_all(self) -> list[FlowContext]:
|
|
121
|
+
results: list[FlowContext] = []
|
|
122
|
+
for flow in self.flows:
|
|
123
|
+
for source_path in self.polling.startup_sources(flow):
|
|
124
|
+
batch_signatures = ()
|
|
125
|
+
trigger = flow.trigger
|
|
126
|
+
if (
|
|
127
|
+
source_path is None
|
|
128
|
+
and isinstance(trigger, WatchSpec)
|
|
129
|
+
and trigger.mode == "poll"
|
|
130
|
+
and trigger.run_as == "batch"
|
|
131
|
+
and trigger.source is not None
|
|
132
|
+
and trigger.source.is_dir()
|
|
133
|
+
):
|
|
134
|
+
batch_signatures = self.polling.stale_batch_poll_signatures(flow)
|
|
135
|
+
results.append(self.run_executor.run_one(flow, source_path, batch_signatures=batch_signatures))
|
|
136
|
+
return results
|
|
137
|
+
|
|
138
|
+
def _preview_one(self, flow: "Flow", source_path: "Path | None", *, use: str | None) -> FlowContext:
|
|
139
|
+
return self.run_executor.preview_one(flow, source_path, use=use)
|
|
140
|
+
|
|
141
|
+
def _make_watcher(self, trigger: WatchSpec) -> PollingWatcher:
|
|
142
|
+
return self.polling.make_watcher(trigger)
|
|
143
|
+
|
|
144
|
+
def _startup_sources(self, flow: "Flow", *, allow_missing: bool = False):
|
|
145
|
+
return self.polling.startup_sources(flow, allow_missing=allow_missing)
|
|
146
|
+
|
|
147
|
+
def _stale_poll_sources(self, flow: "Flow"):
|
|
148
|
+
return self.polling.stale_poll_sources(flow)
|
|
149
|
+
|
|
150
|
+
def _stale_batch_poll_signatures(self, flow: "Flow"):
|
|
151
|
+
return self.polling.stale_batch_poll_signatures(flow)
|
|
152
|
+
|
|
153
|
+
def _is_poll_source_stale(self, flow: "Flow", source_path: "Path | None") -> bool:
|
|
154
|
+
return self.polling.is_poll_source_stale(flow, source_path)
|
|
155
|
+
|
|
156
|
+
def _poll_source_signature(self, flow: "Flow", source_path: "Path | None"):
|
|
157
|
+
return self.polling.poll_source_signature(flow, source_path)
|
|
158
|
+
|
|
159
|
+
def _normalized_source_path(self, source_path: "Path | None"):
|
|
160
|
+
return self.polling.normalized_source_path(source_path)
|
|
161
|
+
|
|
162
|
+
def _check_flow_stop(self) -> None:
|
|
163
|
+
if self.flow_stop_event is not None and self.flow_stop_event.is_set():
|
|
164
|
+
raise FlowStoppedError("Flow stop requested by operator.")
|
|
165
|
+
|
|
166
|
+
def _emit_status(self, message: str) -> None:
|
|
167
|
+
if self.status_callback is not None:
|
|
168
|
+
self.status_callback(message)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = ["RuntimeLedgerService", "_FlowRuntime", "default_runtime_ledger_service"]
|