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.
Files changed (200) hide show
  1. data_engine/__init__.py +37 -0
  2. data_engine/application/__init__.py +39 -0
  3. data_engine/application/actions.py +42 -0
  4. data_engine/application/catalog.py +151 -0
  5. data_engine/application/control.py +213 -0
  6. data_engine/application/details.py +73 -0
  7. data_engine/application/runtime.py +449 -0
  8. data_engine/application/workspace.py +62 -0
  9. data_engine/authoring/__init__.py +14 -0
  10. data_engine/authoring/builder.py +31 -0
  11. data_engine/authoring/execution/__init__.py +6 -0
  12. data_engine/authoring/execution/app.py +6 -0
  13. data_engine/authoring/execution/context.py +82 -0
  14. data_engine/authoring/execution/continuous.py +176 -0
  15. data_engine/authoring/execution/grouped.py +106 -0
  16. data_engine/authoring/execution/logging.py +83 -0
  17. data_engine/authoring/execution/polling.py +135 -0
  18. data_engine/authoring/execution/runner.py +210 -0
  19. data_engine/authoring/execution/single.py +171 -0
  20. data_engine/authoring/flow.py +361 -0
  21. data_engine/authoring/helpers.py +160 -0
  22. data_engine/authoring/model.py +59 -0
  23. data_engine/authoring/primitives.py +430 -0
  24. data_engine/authoring/services.py +42 -0
  25. data_engine/devtools/__init__.py +3 -0
  26. data_engine/devtools/project_ast_map.py +503 -0
  27. data_engine/docs/__init__.py +1 -0
  28. data_engine/docs/sphinx_source/_static/custom.css +13 -0
  29. data_engine/docs/sphinx_source/api.rst +42 -0
  30. data_engine/docs/sphinx_source/conf.py +37 -0
  31. data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
  32. data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
  33. data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
  34. data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
  35. data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
  36. data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
  37. data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
  38. data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
  39. data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
  40. data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
  41. data_engine/docs/sphinx_source/guides/project-map.md +118 -0
  42. data_engine/docs/sphinx_source/guides/recipes.md +268 -0
  43. data_engine/docs/sphinx_source/index.rst +22 -0
  44. data_engine/domain/__init__.py +92 -0
  45. data_engine/domain/actions.py +69 -0
  46. data_engine/domain/catalog.py +128 -0
  47. data_engine/domain/details.py +214 -0
  48. data_engine/domain/diagnostics.py +56 -0
  49. data_engine/domain/errors.py +104 -0
  50. data_engine/domain/inspection.py +99 -0
  51. data_engine/domain/logs.py +118 -0
  52. data_engine/domain/operations.py +172 -0
  53. data_engine/domain/operator.py +72 -0
  54. data_engine/domain/runs.py +155 -0
  55. data_engine/domain/runtime.py +279 -0
  56. data_engine/domain/source_state.py +17 -0
  57. data_engine/domain/support.py +54 -0
  58. data_engine/domain/time.py +23 -0
  59. data_engine/domain/workspace.py +159 -0
  60. data_engine/flow_modules/__init__.py +1 -0
  61. data_engine/flow_modules/flow_module_compiler.py +179 -0
  62. data_engine/flow_modules/flow_module_loader.py +201 -0
  63. data_engine/helpers/__init__.py +25 -0
  64. data_engine/helpers/duckdb.py +705 -0
  65. data_engine/hosts/__init__.py +1 -0
  66. data_engine/hosts/daemon/__init__.py +23 -0
  67. data_engine/hosts/daemon/app.py +221 -0
  68. data_engine/hosts/daemon/bootstrap.py +69 -0
  69. data_engine/hosts/daemon/client.py +465 -0
  70. data_engine/hosts/daemon/commands.py +64 -0
  71. data_engine/hosts/daemon/composition.py +310 -0
  72. data_engine/hosts/daemon/constants.py +15 -0
  73. data_engine/hosts/daemon/entrypoints.py +97 -0
  74. data_engine/hosts/daemon/lifecycle.py +191 -0
  75. data_engine/hosts/daemon/manager.py +272 -0
  76. data_engine/hosts/daemon/ownership.py +126 -0
  77. data_engine/hosts/daemon/runtime_commands.py +188 -0
  78. data_engine/hosts/daemon/runtime_control.py +31 -0
  79. data_engine/hosts/daemon/server.py +84 -0
  80. data_engine/hosts/daemon/shared_state.py +147 -0
  81. data_engine/hosts/daemon/state_sync.py +101 -0
  82. data_engine/platform/__init__.py +1 -0
  83. data_engine/platform/identity.py +35 -0
  84. data_engine/platform/local_settings.py +146 -0
  85. data_engine/platform/theme.py +259 -0
  86. data_engine/platform/workspace_models.py +190 -0
  87. data_engine/platform/workspace_policy.py +333 -0
  88. data_engine/runtime/__init__.py +1 -0
  89. data_engine/runtime/file_watch.py +185 -0
  90. data_engine/runtime/ledger_models.py +116 -0
  91. data_engine/runtime/runtime_db.py +938 -0
  92. data_engine/runtime/shared_state.py +523 -0
  93. data_engine/services/__init__.py +49 -0
  94. data_engine/services/daemon.py +64 -0
  95. data_engine/services/daemon_state.py +40 -0
  96. data_engine/services/flow_catalog.py +102 -0
  97. data_engine/services/flow_execution.py +48 -0
  98. data_engine/services/ledger.py +85 -0
  99. data_engine/services/logs.py +65 -0
  100. data_engine/services/runtime_binding.py +105 -0
  101. data_engine/services/runtime_execution.py +126 -0
  102. data_engine/services/runtime_history.py +62 -0
  103. data_engine/services/settings.py +58 -0
  104. data_engine/services/shared_state.py +28 -0
  105. data_engine/services/theme.py +59 -0
  106. data_engine/services/workspace_provisioning.py +224 -0
  107. data_engine/services/workspaces.py +74 -0
  108. data_engine/ui/__init__.py +3 -0
  109. data_engine/ui/cli/__init__.py +19 -0
  110. data_engine/ui/cli/app.py +161 -0
  111. data_engine/ui/cli/commands_doctor.py +178 -0
  112. data_engine/ui/cli/commands_run.py +80 -0
  113. data_engine/ui/cli/commands_start.py +100 -0
  114. data_engine/ui/cli/commands_workspace.py +97 -0
  115. data_engine/ui/cli/dependencies.py +44 -0
  116. data_engine/ui/cli/parser.py +56 -0
  117. data_engine/ui/gui/__init__.py +25 -0
  118. data_engine/ui/gui/app.py +116 -0
  119. data_engine/ui/gui/bootstrap.py +487 -0
  120. data_engine/ui/gui/bootstrapper.py +140 -0
  121. data_engine/ui/gui/cache_models.py +23 -0
  122. data_engine/ui/gui/control_support.py +185 -0
  123. data_engine/ui/gui/controllers/__init__.py +6 -0
  124. data_engine/ui/gui/controllers/flows.py +439 -0
  125. data_engine/ui/gui/controllers/runtime.py +245 -0
  126. data_engine/ui/gui/dialogs/__init__.py +12 -0
  127. data_engine/ui/gui/dialogs/messages.py +88 -0
  128. data_engine/ui/gui/dialogs/previews.py +222 -0
  129. data_engine/ui/gui/helpers/__init__.py +62 -0
  130. data_engine/ui/gui/helpers/inspection.py +81 -0
  131. data_engine/ui/gui/helpers/lifecycle.py +112 -0
  132. data_engine/ui/gui/helpers/scroll.py +28 -0
  133. data_engine/ui/gui/helpers/theming.py +87 -0
  134. data_engine/ui/gui/icons/dark_light.svg +12 -0
  135. data_engine/ui/gui/icons/documentation.svg +1 -0
  136. data_engine/ui/gui/icons/failed.svg +3 -0
  137. data_engine/ui/gui/icons/group.svg +4 -0
  138. data_engine/ui/gui/icons/home.svg +2 -0
  139. data_engine/ui/gui/icons/manual.svg +2 -0
  140. data_engine/ui/gui/icons/poll.svg +2 -0
  141. data_engine/ui/gui/icons/schedule.svg +4 -0
  142. data_engine/ui/gui/icons/settings.svg +2 -0
  143. data_engine/ui/gui/icons/started.svg +3 -0
  144. data_engine/ui/gui/icons/success.svg +3 -0
  145. data_engine/ui/gui/icons/view-log.svg +3 -0
  146. data_engine/ui/gui/icons.py +50 -0
  147. data_engine/ui/gui/launcher.py +48 -0
  148. data_engine/ui/gui/presenters/__init__.py +72 -0
  149. data_engine/ui/gui/presenters/docs.py +140 -0
  150. data_engine/ui/gui/presenters/logs.py +58 -0
  151. data_engine/ui/gui/presenters/runtime_projection.py +29 -0
  152. data_engine/ui/gui/presenters/sidebar.py +88 -0
  153. data_engine/ui/gui/presenters/steps.py +148 -0
  154. data_engine/ui/gui/presenters/workspace.py +39 -0
  155. data_engine/ui/gui/presenters/workspace_binding.py +75 -0
  156. data_engine/ui/gui/presenters/workspace_settings.py +182 -0
  157. data_engine/ui/gui/preview_models.py +37 -0
  158. data_engine/ui/gui/render_support.py +241 -0
  159. data_engine/ui/gui/rendering/__init__.py +12 -0
  160. data_engine/ui/gui/rendering/artifacts.py +95 -0
  161. data_engine/ui/gui/rendering/icons.py +50 -0
  162. data_engine/ui/gui/runtime.py +47 -0
  163. data_engine/ui/gui/state_support.py +193 -0
  164. data_engine/ui/gui/support.py +214 -0
  165. data_engine/ui/gui/surface.py +209 -0
  166. data_engine/ui/gui/theme.py +720 -0
  167. data_engine/ui/gui/widgets/__init__.py +34 -0
  168. data_engine/ui/gui/widgets/config.py +41 -0
  169. data_engine/ui/gui/widgets/logs.py +62 -0
  170. data_engine/ui/gui/widgets/panels.py +507 -0
  171. data_engine/ui/gui/widgets/sidebar.py +130 -0
  172. data_engine/ui/gui/widgets/steps.py +84 -0
  173. data_engine/ui/tui/__init__.py +5 -0
  174. data_engine/ui/tui/app.py +222 -0
  175. data_engine/ui/tui/bootstrap.py +475 -0
  176. data_engine/ui/tui/bootstrapper.py +117 -0
  177. data_engine/ui/tui/controllers/__init__.py +6 -0
  178. data_engine/ui/tui/controllers/flows.py +349 -0
  179. data_engine/ui/tui/controllers/runtime.py +167 -0
  180. data_engine/ui/tui/runtime.py +34 -0
  181. data_engine/ui/tui/state_support.py +141 -0
  182. data_engine/ui/tui/support.py +63 -0
  183. data_engine/ui/tui/theme.py +204 -0
  184. data_engine/ui/tui/widgets.py +123 -0
  185. data_engine/views/__init__.py +109 -0
  186. data_engine/views/actions.py +80 -0
  187. data_engine/views/artifacts.py +58 -0
  188. data_engine/views/flow_display.py +69 -0
  189. data_engine/views/logs.py +54 -0
  190. data_engine/views/models.py +96 -0
  191. data_engine/views/presentation.py +133 -0
  192. data_engine/views/runs.py +62 -0
  193. data_engine/views/state.py +39 -0
  194. data_engine/views/status.py +13 -0
  195. data_engine/views/text.py +109 -0
  196. py_data_engine-0.1.0.dist-info/METADATA +330 -0
  197. py_data_engine-0.1.0.dist-info/RECORD +200 -0
  198. py_data_engine-0.1.0.dist-info/WHEEL +5 -0
  199. py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
  200. 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"]