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,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
|
+
]
|