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,361 @@
1
+ """Flow DSL and public authoring entrypoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ import inspect
7
+ from pathlib import Path
8
+
9
+ from data_engine.authoring.helpers import (
10
+ _callable_identifier,
11
+ _callable_name,
12
+ _normalize_extensions,
13
+ _normalize_watch_times,
14
+ _parse_duration,
15
+ _parse_schedule_at,
16
+ _resolve_flow_path,
17
+ _validate_label,
18
+ _validate_slot_name,
19
+ )
20
+ from data_engine.authoring.model import FlowValidationError
21
+ from data_engine.authoring.primitives import Batch, FlowContext, MirrorSpec, StepSpec, WatchSpec, collect_files
22
+ from data_engine.flow_modules.flow_module_loader import (
23
+ in_compiled_flow_module_context,
24
+ )
25
+ from data_engine.authoring.services import AuthoringServices, build_authoring_services, default_authoring_services
26
+ from data_engine.services.flow_execution import FlowExecutionService
27
+ from data_engine.services.runtime_execution import RuntimeExecutionService
28
+
29
+
30
+ def _resolve_authoring_services(
31
+ *,
32
+ authoring_services: AuthoringServices | None = None,
33
+ runtime_execution_service: RuntimeExecutionService | None = None,
34
+ flow_execution_service: FlowExecutionService | None = None,
35
+ ) -> AuthoringServices:
36
+ """Return one authoring collaborator bundle with explicit overrides applied."""
37
+ services = authoring_services or default_authoring_services()
38
+ if runtime_execution_service is None and flow_execution_service is None:
39
+ return services
40
+ return build_authoring_services(
41
+ runtime_execution_service=runtime_execution_service or services.runtime_execution_service,
42
+ flow_execution_service=flow_execution_service or services.flow_execution_service,
43
+ )
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class Flow:
48
+ """Immutable fluent builder for generic runtime flows."""
49
+
50
+ group: str
51
+ name: str | None = None
52
+ label: str | None = None
53
+ trigger: WatchSpec | None = None
54
+ mirror_spec: MirrorSpec | None = None
55
+ steps: tuple[StepSpec, ...] = ()
56
+ _workspace_root: Path | None = None
57
+
58
+ def __post_init__(self) -> None:
59
+ if self.name is not None and (not isinstance(self.name, str) or not self.name.strip()):
60
+ raise FlowValidationError("Flow name must be a non-empty string when provided.")
61
+ if self.label is not None and (not isinstance(self.label, str) or not self.label.strip()):
62
+ raise FlowValidationError("Flow label must be a non-empty string when provided.")
63
+ if not isinstance(self.group, str) or not self.group.strip():
64
+ raise FlowValidationError("Flow group must be a non-empty string.")
65
+
66
+ def _clone(self, **kwargs) -> "Flow":
67
+ return replace(self, **kwargs)
68
+
69
+ def _append(self, step: StepSpec) -> "Flow":
70
+ return self._clone(steps=(*self.steps, step))
71
+
72
+ def watch(
73
+ self,
74
+ *,
75
+ mode: str,
76
+ run_as: str = "individual",
77
+ source: str | Path | None = None,
78
+ interval: str | None = None,
79
+ time: str | tuple[str, ...] | list[str] | set[str] | None = None,
80
+ extensions: tuple[str, ...] | list[str] | set[str] | None = None,
81
+ settle: int = 1,
82
+ ) -> "Flow":
83
+ normalized_mode = str(mode).strip().lower()
84
+ if normalized_mode not in {"manual", "poll", "schedule"}:
85
+ raise FlowValidationError("watch() mode must be one of 'manual', 'poll', or 'schedule'.")
86
+
87
+ normalized_run_as = str(run_as).strip().lower()
88
+ if normalized_run_as not in {"individual", "batch"}:
89
+ raise FlowValidationError("watch() run_as must be either 'individual' or 'batch'.")
90
+
91
+ if not isinstance(settle, int) or settle < 0:
92
+ raise FlowValidationError("watch() settle must be an integer greater than or equal to zero.")
93
+
94
+ resolved_source = _resolve_flow_path(source) if source is not None else None
95
+ normalized_extensions = _normalize_extensions(extensions)
96
+
97
+ if normalized_mode == "manual":
98
+ if interval is not None or time is not None:
99
+ raise FlowValidationError("watch(mode='manual') does not accept interval= or time=.")
100
+ if settle != 1:
101
+ raise FlowValidationError("watch(mode='manual') does not accept settle=.")
102
+ return self._clone(
103
+ trigger=WatchSpec(
104
+ mode="manual",
105
+ run_as=normalized_run_as,
106
+ source=resolved_source,
107
+ extensions=normalized_extensions,
108
+ )
109
+ )
110
+
111
+ if normalized_mode == "poll":
112
+ if resolved_source is None:
113
+ raise FlowValidationError("watch(mode='poll') requires source=.")
114
+ if interval is None:
115
+ raise FlowValidationError("watch(mode='poll') requires interval=.")
116
+ if time is not None:
117
+ raise FlowValidationError("watch(mode='poll') does not accept time=.")
118
+ return self._clone(
119
+ trigger=WatchSpec(
120
+ mode="poll",
121
+ run_as=normalized_run_as,
122
+ source=resolved_source,
123
+ interval=interval,
124
+ interval_seconds=_parse_duration(interval),
125
+ extensions=normalized_extensions,
126
+ settle=settle,
127
+ )
128
+ )
129
+
130
+ if (interval is None) == (time is None):
131
+ raise FlowValidationError("watch(mode='schedule') accepts exactly one of interval= or time=.")
132
+ if settle != 1:
133
+ raise FlowValidationError("watch(mode='schedule') does not accept settle=.")
134
+ if interval is not None:
135
+ return self._clone(
136
+ trigger=WatchSpec(
137
+ mode="schedule",
138
+ run_as=normalized_run_as,
139
+ source=resolved_source,
140
+ interval=interval,
141
+ interval_seconds=_parse_duration(interval),
142
+ extensions=normalized_extensions,
143
+ )
144
+ )
145
+ assert time is not None
146
+ time_values = _normalize_watch_times(time)
147
+ return self._clone(
148
+ trigger=WatchSpec(
149
+ mode="schedule",
150
+ run_as=normalized_run_as,
151
+ source=resolved_source,
152
+ time=time_values[0] if len(time_values) == 1 else time_values,
153
+ times=time_values,
154
+ time_slots=tuple(_parse_schedule_at(value) for value in time_values),
155
+ extensions=normalized_extensions,
156
+ )
157
+ )
158
+
159
+ def mirror(self, *, root: str | Path) -> "Flow":
160
+ """Bind a mirrored output namespace rooted at one directory."""
161
+ return self._clone(mirror_spec=MirrorSpec(root=_resolve_flow_path(root)))
162
+
163
+ def step(
164
+ self,
165
+ fn,
166
+ *,
167
+ use: str | None = None,
168
+ save_as: str | None = None,
169
+ label: str | None = None,
170
+ ) -> "Flow":
171
+ if not callable(fn):
172
+ raise FlowValidationError("step() fn must be callable")
173
+ normalized_use = _validate_slot_name(method_name="step", slot_name="use", value=use)
174
+ normalized_save_as = _validate_slot_name(method_name="step", slot_name="save_as", value=save_as)
175
+ normalized_label = _validate_label(method_name="step", label=label)
176
+ signature = inspect.signature(fn)
177
+ if len(signature.parameters) != 1:
178
+ raise FlowValidationError("step() callables must accept exactly one context parameter.")
179
+ return self._append(
180
+ StepSpec(
181
+ fn=fn,
182
+ use=normalized_use,
183
+ save_as=normalized_save_as,
184
+ label=normalized_label or _callable_name(fn),
185
+ function_name=_callable_identifier(fn),
186
+ )
187
+ )
188
+
189
+ def map(
190
+ self,
191
+ fn,
192
+ *,
193
+ use: str | None = None,
194
+ save_as: str | None = None,
195
+ label: str | None = None,
196
+ ) -> "Flow":
197
+ if not callable(fn):
198
+ raise FlowValidationError("map() fn must be callable")
199
+ normalized_use = _validate_slot_name(method_name="map", slot_name="use", value=use)
200
+ normalized_save_as = _validate_slot_name(method_name="map", slot_name="save_as", value=save_as)
201
+ normalized_label = _validate_label(method_name="map", label=label)
202
+ signature = inspect.signature(fn)
203
+ parameter_count = len(signature.parameters)
204
+ if parameter_count not in {1, 2}:
205
+ raise FlowValidationError("map() callables must accept either (item) or (context, item).")
206
+
207
+ def _run_each(context: FlowContext):
208
+ current = context.current
209
+ if isinstance(current, Batch):
210
+ items = current.items
211
+ elif current is None or isinstance(current, (str, bytes, dict)):
212
+ raise FlowValidationError("map() requires an iterable current value.")
213
+ else:
214
+ try:
215
+ items = tuple(current)
216
+ except TypeError as exc:
217
+ raise FlowValidationError("map() requires an iterable current value.") from exc
218
+ if not items:
219
+ raise FlowValidationError("map() requires at least one item.")
220
+ if parameter_count == 1:
221
+ return Batch(tuple(fn(item) for item in items))
222
+ return Batch(tuple(fn(context, item) for item in items))
223
+
224
+ return self._append(
225
+ StepSpec(
226
+ fn=_run_each,
227
+ use=normalized_use,
228
+ save_as=normalized_save_as,
229
+ label=normalized_label or _callable_name(fn),
230
+ function_name=_callable_identifier(fn),
231
+ )
232
+ )
233
+
234
+ def collect(
235
+ self,
236
+ extensions: tuple[str, ...] | list[str] | set[str],
237
+ *,
238
+ root: str | Path | None = None,
239
+ recursive: bool = False,
240
+ use: str | None = None,
241
+ save_as: str | None = None,
242
+ label: str | None = None,
243
+ ) -> "Flow":
244
+ normalized_use = _validate_slot_name(method_name="collect", slot_name="use", value=use)
245
+ normalized_save_as = _validate_slot_name(method_name="collect", slot_name="save_as", value=save_as)
246
+ normalized_label = _validate_label(method_name="collect", label=label)
247
+ return self.step(
248
+ collect_files(extensions, root=root, recursive=recursive),
249
+ use=normalized_use,
250
+ save_as=normalized_save_as,
251
+ label=normalized_label or "Collect Files",
252
+ )
253
+
254
+ def step_each(
255
+ self,
256
+ fn,
257
+ *,
258
+ use: str | None = None,
259
+ save_as: str | None = None,
260
+ label: str | None = None,
261
+ ) -> "Flow":
262
+ return self.map(fn, use=use, save_as=save_as, label=label)
263
+
264
+ @property
265
+ def mode(self) -> str:
266
+ if isinstance(self.trigger, WatchSpec):
267
+ return self.trigger.mode
268
+ return "manual"
269
+
270
+ def run_once(
271
+ self,
272
+ *,
273
+ authoring_services: AuthoringServices | None = None,
274
+ runtime_execution_service: RuntimeExecutionService | None = None,
275
+ ) -> list[FlowContext]:
276
+ service = _resolve_authoring_services(
277
+ authoring_services=authoring_services,
278
+ runtime_execution_service=runtime_execution_service,
279
+ ).runtime_execution_service
280
+ return service.run_once(self)
281
+
282
+ def preview(
283
+ self,
284
+ *,
285
+ use: str | None = None,
286
+ authoring_services: AuthoringServices | None = None,
287
+ runtime_execution_service: RuntimeExecutionService | None = None,
288
+ ):
289
+ if in_compiled_flow_module_context():
290
+ raise FlowValidationError("preview() is not available inside compiled flow modules.")
291
+ normalized_use = _validate_slot_name(method_name="preview", slot_name="use", value=use)
292
+ service = _resolve_authoring_services(
293
+ authoring_services=authoring_services,
294
+ runtime_execution_service=runtime_execution_service,
295
+ ).runtime_execution_service
296
+ return service.preview(self, use=normalized_use)
297
+
298
+ def show(self):
299
+ if in_compiled_flow_module_context():
300
+ raise FlowValidationError("show() is not available inside compiled flow modules.")
301
+ results = self.run_once()
302
+ if len(results) != 1:
303
+ raise FlowValidationError(f"show() requires exactly one result, found {len(results)}.")
304
+ return results[0].current
305
+
306
+ def run(
307
+ self,
308
+ *,
309
+ authoring_services: AuthoringServices | None = None,
310
+ runtime_execution_service: RuntimeExecutionService | None = None,
311
+ ) -> list[FlowContext]:
312
+ service = _resolve_authoring_services(
313
+ authoring_services=authoring_services,
314
+ runtime_execution_service=runtime_execution_service,
315
+ ).runtime_execution_service
316
+ return service.run_continuous(self)
317
+
318
+
319
+ def load_flow(
320
+ name: str,
321
+ *,
322
+ data_root: Path | None = None,
323
+ authoring_services: AuthoringServices | None = None,
324
+ flow_execution_service: FlowExecutionService | None = None,
325
+ ) -> Flow:
326
+ """Load one code-defined flow by flow-module name."""
327
+ service = _resolve_authoring_services(
328
+ authoring_services=authoring_services,
329
+ flow_execution_service=flow_execution_service,
330
+ ).flow_execution_service
331
+ return service.load_flow(name, workspace_root=data_root)
332
+
333
+
334
+ def discover_flows(
335
+ *,
336
+ data_root: Path | None = None,
337
+ authoring_services: AuthoringServices | None = None,
338
+ flow_execution_service: FlowExecutionService | None = None,
339
+ ) -> tuple[Flow, ...]:
340
+ """Discover and build all code-defined flows from compiled flow modules."""
341
+ service = _resolve_authoring_services(
342
+ authoring_services=authoring_services,
343
+ flow_execution_service=flow_execution_service,
344
+ ).flow_execution_service
345
+ return service.discover_flows(workspace_root=data_root)
346
+
347
+
348
+ def run(
349
+ *flows: Flow,
350
+ authoring_services: AuthoringServices | None = None,
351
+ runtime_execution_service: RuntimeExecutionService | None = None,
352
+ ) -> list[FlowContext]:
353
+ """Run multiple flows with sequential execution per group and parallel groups."""
354
+ service = _resolve_authoring_services(
355
+ authoring_services=authoring_services,
356
+ runtime_execution_service=runtime_execution_service,
357
+ ).runtime_execution_service
358
+ return service.run_grouped_continuous(tuple(flows))
359
+
360
+
361
+ __all__ = ["Flow", "discover_flows", "load_flow", "run"]
@@ -0,0 +1,160 @@
1
+ """Shared authoring helper functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from pathlib import Path
7
+ import re
8
+ from typing import Callable
9
+
10
+ from data_engine.authoring.model import FlowValidationError
11
+
12
+
13
+ def _parse_duration(value: str) -> float:
14
+ raw = value.strip().lower()
15
+ units = (
16
+ ("ms", 0.001),
17
+ ("s", 1.0),
18
+ ("m", 60.0),
19
+ ("h", 3600.0),
20
+ ("d", 86400.0),
21
+ ("w", 604800.0),
22
+ )
23
+ for suffix, multiplier in units:
24
+ if raw.endswith(suffix):
25
+ number = raw[: -len(suffix)].strip()
26
+ try:
27
+ parsed = float(number)
28
+ except ValueError as exc:
29
+ raise FlowValidationError(f"Invalid duration: {value!r}") from exc
30
+ if parsed <= 0:
31
+ raise FlowValidationError(f"Duration must be positive: {value!r}")
32
+ return parsed * multiplier
33
+ raise FlowValidationError(f"Unsupported duration format: {value!r}")
34
+
35
+
36
+ def _parse_schedule_at(value: str) -> tuple[int, int]:
37
+ match = re.fullmatch(r"(?P<hour>\d{2}):(?P<minute>\d{2})", value.strip())
38
+ if match is None:
39
+ raise FlowValidationError(f"Invalid schedule time: {value!r}")
40
+ hour = int(match.group("hour"))
41
+ minute = int(match.group("minute"))
42
+ if hour > 23 or minute > 59:
43
+ raise FlowValidationError(f"Invalid schedule time: {value!r}")
44
+ return hour, minute
45
+
46
+
47
+ def _normalize_watch_times(value: str | tuple[str, ...] | list[str] | set[str]) -> tuple[str, ...]:
48
+ if isinstance(value, str):
49
+ raw_values = [value]
50
+ elif isinstance(value, (tuple, list, set)):
51
+ raw_values = [str(item) for item in value]
52
+ else:
53
+ raise FlowValidationError("watch() time must be a time string or a collection of time strings.")
54
+
55
+ if not raw_values:
56
+ raise FlowValidationError("watch() time must include at least one time.")
57
+
58
+ normalized_by_slot: dict[tuple[int, int], str] = {}
59
+ for raw in raw_values:
60
+ hour, minute = _parse_schedule_at(raw)
61
+ normalized_by_slot[(hour, minute)] = f"{hour:02d}:{minute:02d}"
62
+
63
+ return tuple(normalized_by_slot[slot] for slot in sorted(normalized_by_slot))
64
+
65
+
66
+ def _normalize_extensions(extensions: tuple[str, ...] | list[str] | set[str] | None) -> tuple[str, ...] | None:
67
+ if extensions is None:
68
+ return None
69
+ normalized: list[str] = []
70
+ for ext in extensions:
71
+ value = str(ext).strip().lower()
72
+ if not value:
73
+ raise FlowValidationError("Empty extension is not allowed.")
74
+ if not value.startswith("."):
75
+ value = f".{value}"
76
+ normalized.append(value)
77
+ if not normalized:
78
+ raise FlowValidationError("At least one extension is required.")
79
+ return tuple(normalized)
80
+
81
+
82
+ def _title_case_words(value: str, *, empty: str = "Step") -> str:
83
+ if not value:
84
+ return empty
85
+ snake = re.sub(r"[_\s]+", " ", value.strip())
86
+ spaced = re.sub(r"(?<!^)(?=[A-Z])", " ", snake)
87
+ words = [part for part in spaced.split() if part]
88
+ return " ".join(word.capitalize() for word in words) or empty
89
+
90
+
91
+ def _callable_name(fn: Callable[..., object]) -> str:
92
+ name = getattr(fn, "__name__", None)
93
+ if isinstance(name, str) and name and name != "<lambda>":
94
+ return _title_case_words(name)
95
+ if inspect.isclass(fn):
96
+ return _title_case_words(fn.__name__)
97
+ fn_cls = getattr(fn, "__class__", None)
98
+ if fn_cls is not None and getattr(fn_cls, "__name__", "") not in {"function", "method"}:
99
+ return _title_case_words(fn_cls.__name__)
100
+ return "Lambda"
101
+
102
+
103
+ def _callable_identifier(fn: Callable[..., object]) -> str:
104
+ """Return a developer-facing callable identifier when available."""
105
+ name = getattr(fn, "__name__", None)
106
+ if isinstance(name, str) and name:
107
+ return name
108
+ if inspect.isclass(fn):
109
+ return fn.__name__
110
+ fn_cls = getattr(fn, "__class__", None)
111
+ if fn_cls is not None and getattr(fn_cls, "__name__", "") not in {"function", "method"}:
112
+ return fn_cls.__name__
113
+ return "lambda"
114
+
115
+
116
+ def _resolve_flow_path(value: str | Path) -> Path:
117
+ raw = Path(value).expanduser()
118
+ if raw.is_absolute():
119
+ return raw.resolve()
120
+ from data_engine.flow_modules.flow_module_loader import current_compiled_flow_module_dir
121
+
122
+ compiled_flow_module_dir = current_compiled_flow_module_dir()
123
+ if compiled_flow_module_dir is not None:
124
+ return (compiled_flow_module_dir / raw).resolve()
125
+ return raw.resolve()
126
+
127
+
128
+ def _validate_slot_name(*, method_name: str, slot_name: str, value: str | None) -> str | None:
129
+ """Validate and normalize one named runtime object slot reference."""
130
+ if value is None:
131
+ return None
132
+ if not isinstance(value, str) or not value.strip():
133
+ raise FlowValidationError(f"{method_name}() {slot_name} must be a non-empty string.")
134
+ normalized = value.strip()
135
+ if slot_name == "save_as" and normalized == "current":
136
+ raise FlowValidationError(f"{method_name}() save_as cannot overwrite the runtime-owned 'current' slot.")
137
+ return normalized
138
+
139
+
140
+ def _validate_label(*, method_name: str, label: str | None) -> str | None:
141
+ """Validate one optional user-facing step label."""
142
+ if label is None:
143
+ return None
144
+ if not isinstance(label, str) or not label.strip():
145
+ raise FlowValidationError(f"{method_name}() label must be a non-empty string.")
146
+ return label.strip()
147
+
148
+
149
+ __all__ = [
150
+ "_callable_identifier",
151
+ "_callable_name",
152
+ "_normalize_extensions",
153
+ "_normalize_watch_times",
154
+ "_parse_duration",
155
+ "_parse_schedule_at",
156
+ "_resolve_flow_path",
157
+ "_title_case_words",
158
+ "_validate_label",
159
+ "_validate_slot_name",
160
+ ]
@@ -0,0 +1,59 @@
1
+ """Core shared model objects for the fluent flow runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ class FlowValidationError(ValueError):
9
+ """Raised when a flow configuration or runtime input cannot be validated."""
10
+
11
+
12
+ class FlowStoppedError(RuntimeError):
13
+ """Raised when a running flow is stopped by an external control."""
14
+
15
+
16
+ class FlowExecutionError(FlowValidationError):
17
+ """Raised when a flow module fails during import, build, or runtime execution."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ flow_name: str,
23
+ phase: str,
24
+ detail: str,
25
+ step_label: str | None = None,
26
+ function_name: str | None = None,
27
+ source_path: Path | str | None = None,
28
+ ) -> None:
29
+ self.flow_name = flow_name
30
+ self.phase = phase
31
+ self.detail = detail
32
+ self.step_label = step_label
33
+ self.function_name = function_name
34
+ self.source_path = str(source_path) if source_path is not None else None
35
+ super().__init__(self._render())
36
+
37
+ def _render(self) -> str:
38
+ if self.phase == "step":
39
+ message = f'Flow "{self.flow_name}" failed in step "{self.step_label or "Unknown Step"}"'
40
+ if self.function_name:
41
+ message = f"{message} (function {self.function_name})"
42
+ if self.source_path:
43
+ message = f'{message} for source "{self.source_path}"'
44
+ return f"{message}: {self.detail}"
45
+ if self.phase == "build":
46
+ if self.function_name:
47
+ return f'Flow module "{self.flow_name}" failed during build() in {self.function_name}: {self.detail}'
48
+ return f'Flow module "{self.flow_name}" failed during build(): {self.detail}'
49
+ if self.phase == "import":
50
+ return f'Flow module "{self.flow_name}" failed during import: {self.detail}'
51
+ if self.phase == "compile":
52
+ return f'Flow module "{self.flow_name}" failed during compilation: {self.detail}'
53
+ return f'Flow module "{self.flow_name}" failed during {self.phase}: {self.detail}'
54
+
55
+ __all__ = [
56
+ "FlowExecutionError",
57
+ "FlowStoppedError",
58
+ "FlowValidationError",
59
+ ]