flowyml 1.4.0__py3-none-any.whl → 1.6.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.
- flowyml/__init__.py +2 -1
- flowyml/assets/featureset.py +30 -5
- flowyml/assets/metrics.py +47 -4
- flowyml/cli/main.py +21 -0
- flowyml/cli/models.py +444 -0
- flowyml/cli/rich_utils.py +95 -0
- flowyml/core/checkpoint.py +6 -1
- flowyml/core/conditional.py +104 -0
- flowyml/core/display.py +525 -0
- flowyml/core/execution_status.py +1 -0
- flowyml/core/executor.py +201 -8
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +301 -11
- flowyml/core/project.py +4 -1
- flowyml/core/scheduler.py +225 -81
- flowyml/core/versioning.py +13 -4
- flowyml/registry/model_registry.py +1 -1
- flowyml/storage/sql.py +53 -13
- flowyml/ui/backend/main.py +2 -0
- flowyml/ui/backend/routers/assets.py +36 -0
- flowyml/ui/backend/routers/execution.py +2 -2
- flowyml/ui/backend/routers/runs.py +211 -0
- flowyml/ui/backend/routers/stats.py +2 -2
- flowyml/ui/backend/routers/websocket.py +121 -0
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/package-lock.json +289 -0
- flowyml/ui/frontend/package.json +1 -0
- flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
- flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
- flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
- flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/server_manager.py +181 -0
- flowyml/ui/utils.py +63 -1
- flowyml/utils/config.py +7 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/executor.py
CHANGED
|
@@ -7,6 +7,140 @@ from typing import Any
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
|
|
10
|
+
import threading
|
|
11
|
+
import ctypes
|
|
12
|
+
import requests
|
|
13
|
+
import os
|
|
14
|
+
import inspect
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StopExecutionError(Exception):
|
|
18
|
+
"""Exception raised when execution is stopped externally."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Alias for backwards compatibility
|
|
24
|
+
StopExecution = StopExecutionError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _async_raise(tid, exctype):
|
|
28
|
+
"""Raises an exception in the threads with id tid"""
|
|
29
|
+
if not inspect.isclass(exctype):
|
|
30
|
+
raise TypeError("Only types can be raised (not instances)")
|
|
31
|
+
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype))
|
|
32
|
+
if res == 0:
|
|
33
|
+
raise ValueError("invalid thread id")
|
|
34
|
+
if res != 1:
|
|
35
|
+
# """if it returns a number greater than one, you're in trouble,
|
|
36
|
+
# and you should call it again with exc=NULL to revert the effect"""
|
|
37
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), None)
|
|
38
|
+
raise SystemError("PyThreadState_SetAsyncExc failed")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LogCapture:
|
|
42
|
+
"""Context manager to capture stdout/stderr for streaming to the server."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self._buffer = []
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
def write(self, text):
|
|
49
|
+
if text.strip():
|
|
50
|
+
with self._lock:
|
|
51
|
+
self._buffer.append(text)
|
|
52
|
+
|
|
53
|
+
def flush(self):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def get_and_clear(self) -> list[str]:
|
|
57
|
+
with self._lock:
|
|
58
|
+
lines = self._buffer[:]
|
|
59
|
+
self._buffer.clear()
|
|
60
|
+
return lines
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MonitorThread(threading.Thread):
|
|
64
|
+
"""Background thread that sends heartbeats and flushes logs to the server."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
run_id: str,
|
|
69
|
+
step_name: str,
|
|
70
|
+
target_tid: int,
|
|
71
|
+
log_capture: LogCapture | None = None,
|
|
72
|
+
interval: int = 5,
|
|
73
|
+
):
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.run_id = run_id
|
|
76
|
+
self.step_name = step_name
|
|
77
|
+
self.target_tid = target_tid
|
|
78
|
+
self.log_capture = log_capture
|
|
79
|
+
self.interval = interval
|
|
80
|
+
self._stop_event = threading.Event()
|
|
81
|
+
# Get UI server URL from configuration (supports env vars, config, centralized deployments)
|
|
82
|
+
try:
|
|
83
|
+
from flowyml.ui.utils import get_ui_server_url
|
|
84
|
+
|
|
85
|
+
self.api_url = get_ui_server_url()
|
|
86
|
+
except Exception:
|
|
87
|
+
# Fallback to environment variable or default
|
|
88
|
+
self.api_url = os.getenv("FLOWYML_SERVER_URL", "http://localhost:8080")
|
|
89
|
+
|
|
90
|
+
def stop(self):
|
|
91
|
+
self._stop_event.set()
|
|
92
|
+
|
|
93
|
+
def _flush_logs(self):
|
|
94
|
+
"""Send captured logs to the server."""
|
|
95
|
+
if not self.log_capture:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
lines = self.log_capture.get_and_clear()
|
|
99
|
+
if not lines:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
content = "".join(lines)
|
|
103
|
+
with contextlib.suppress(Exception):
|
|
104
|
+
requests.post(
|
|
105
|
+
f"{self.api_url}/api/runs/{self.run_id}/steps/{self.step_name}/logs",
|
|
106
|
+
json={
|
|
107
|
+
"content": content,
|
|
108
|
+
"level": "INFO",
|
|
109
|
+
"timestamp": datetime.now().isoformat(),
|
|
110
|
+
},
|
|
111
|
+
timeout=2,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def run(self):
|
|
115
|
+
while not self._stop_event.is_set():
|
|
116
|
+
try:
|
|
117
|
+
# Send heartbeat
|
|
118
|
+
response = requests.post(
|
|
119
|
+
f"{self.api_url}/api/runs/{self.run_id}/steps/{self.step_name}/heartbeat",
|
|
120
|
+
json={"step_name": self.step_name, "status": "running"},
|
|
121
|
+
timeout=2,
|
|
122
|
+
)
|
|
123
|
+
if response.status_code == 200:
|
|
124
|
+
data = response.json()
|
|
125
|
+
if data.get("action") == "stop":
|
|
126
|
+
print(f"Received stop signal for step {self.step_name}")
|
|
127
|
+
_async_raise(self.target_tid, StopExecution)
|
|
128
|
+
break
|
|
129
|
+
except Exception:
|
|
130
|
+
pass # Ignore heartbeat failures
|
|
131
|
+
|
|
132
|
+
# Flush logs
|
|
133
|
+
self._flush_logs()
|
|
134
|
+
|
|
135
|
+
self._stop_event.wait(self.interval)
|
|
136
|
+
|
|
137
|
+
# Final log flush
|
|
138
|
+
self._flush_logs()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Keep HeartbeatThread as an alias for backwards compatibility
|
|
142
|
+
HeartbeatThread = MonitorThread
|
|
143
|
+
|
|
10
144
|
|
|
11
145
|
@dataclass
|
|
12
146
|
class ExecutionResult:
|
|
@@ -55,7 +189,8 @@ class Executor:
|
|
|
55
189
|
self,
|
|
56
190
|
step_group, # StepGroup
|
|
57
191
|
inputs: dict[str, Any],
|
|
58
|
-
|
|
192
|
+
context: Any | None = None, # Context object for per-step injection
|
|
193
|
+
context_params: dict[str, Any] | None = None, # Deprecated: use context instead
|
|
59
194
|
cache_store: Any | None = None,
|
|
60
195
|
artifact_store: Any | None = None,
|
|
61
196
|
run_id: str | None = None,
|
|
@@ -66,7 +201,8 @@ class Executor:
|
|
|
66
201
|
Args:
|
|
67
202
|
step_group: StepGroup to execute
|
|
68
203
|
inputs: Input data available to the group
|
|
69
|
-
|
|
204
|
+
context: Context object for per-step parameter injection (preferred)
|
|
205
|
+
context_params: Parameters from context (deprecated, use context instead)
|
|
70
206
|
cache_store: Cache store for caching
|
|
71
207
|
artifact_store: Artifact store for materialization
|
|
72
208
|
run_id: Run identifier
|
|
@@ -103,8 +239,6 @@ class LocalExecutor(Executor):
|
|
|
103
239
|
# or just pass what we can.
|
|
104
240
|
# A simple approach: pass nothing if it takes no args, or kwargs if it does.
|
|
105
241
|
# But inspect is safer.
|
|
106
|
-
import inspect
|
|
107
|
-
|
|
108
242
|
sig = inspect.signature(step.condition)
|
|
109
243
|
kwargs = {**inputs, **context_params}
|
|
110
244
|
|
|
@@ -157,7 +291,54 @@ class LocalExecutor(Executor):
|
|
|
157
291
|
kwargs = {**inputs, **context_params}
|
|
158
292
|
|
|
159
293
|
# Execute step
|
|
160
|
-
|
|
294
|
+
monitor_thread = None
|
|
295
|
+
log_capture = None
|
|
296
|
+
original_stdout = None
|
|
297
|
+
original_stderr = None
|
|
298
|
+
try:
|
|
299
|
+
# Start monitoring thread with log capture if run_id is present
|
|
300
|
+
if run_id:
|
|
301
|
+
import sys
|
|
302
|
+
|
|
303
|
+
log_capture = LogCapture()
|
|
304
|
+
original_stdout = sys.stdout
|
|
305
|
+
original_stderr = sys.stderr
|
|
306
|
+
sys.stdout = log_capture
|
|
307
|
+
sys.stderr = log_capture
|
|
308
|
+
|
|
309
|
+
monitor_thread = MonitorThread(
|
|
310
|
+
run_id=run_id,
|
|
311
|
+
step_name=step.name,
|
|
312
|
+
target_tid=threading.get_ident(),
|
|
313
|
+
log_capture=log_capture,
|
|
314
|
+
)
|
|
315
|
+
monitor_thread.start()
|
|
316
|
+
|
|
317
|
+
result = step.func(**kwargs)
|
|
318
|
+
except StopExecution:
|
|
319
|
+
duration = time.time() - start_time
|
|
320
|
+
return ExecutionResult(
|
|
321
|
+
step_name=step.name,
|
|
322
|
+
success=False,
|
|
323
|
+
error="Execution stopped by user",
|
|
324
|
+
duration_seconds=duration,
|
|
325
|
+
retries=retries,
|
|
326
|
+
)
|
|
327
|
+
finally:
|
|
328
|
+
# Restore stdout/stderr
|
|
329
|
+
if original_stdout:
|
|
330
|
+
import sys
|
|
331
|
+
|
|
332
|
+
sys.stdout = original_stdout
|
|
333
|
+
if original_stderr:
|
|
334
|
+
import sys
|
|
335
|
+
|
|
336
|
+
sys.stderr = original_stderr
|
|
337
|
+
|
|
338
|
+
# Stop monitor thread
|
|
339
|
+
if monitor_thread:
|
|
340
|
+
monitor_thread.stop()
|
|
341
|
+
monitor_thread.join()
|
|
161
342
|
|
|
162
343
|
# Materialize output if artifact store is available
|
|
163
344
|
artifact_uri = None
|
|
@@ -225,7 +406,8 @@ class LocalExecutor(Executor):
|
|
|
225
406
|
self,
|
|
226
407
|
step_group, # StepGroup from step_grouping module
|
|
227
408
|
inputs: dict[str, Any],
|
|
228
|
-
|
|
409
|
+
context: Any | None = None, # Context object for per-step injection
|
|
410
|
+
context_params: dict[str, Any] | None = None, # Deprecated: use context instead
|
|
229
411
|
cache_store: Any | None = None,
|
|
230
412
|
artifact_store: Any | None = None,
|
|
231
413
|
run_id: str | None = None,
|
|
@@ -238,7 +420,8 @@ class LocalExecutor(Executor):
|
|
|
238
420
|
Args:
|
|
239
421
|
step_group: StepGroup containing steps to execute
|
|
240
422
|
inputs: Input data available to the group
|
|
241
|
-
|
|
423
|
+
context: Context object for per-step parameter injection (preferred)
|
|
424
|
+
context_params: Parameters from context (deprecated, use context instead)
|
|
242
425
|
cache_store: Cache store for caching
|
|
243
426
|
artifact_store: Artifact store for materialization
|
|
244
427
|
run_id: Run identifier
|
|
@@ -261,11 +444,21 @@ class LocalExecutor(Executor):
|
|
|
261
444
|
if input_name in step_outputs:
|
|
262
445
|
step_inputs[input_name] = step_outputs[input_name]
|
|
263
446
|
|
|
447
|
+
# Inject context parameters for this specific step
|
|
448
|
+
if context is not None:
|
|
449
|
+
# Use context object to inject params per step
|
|
450
|
+
step_context_params = context.inject_params(step.func)
|
|
451
|
+
elif context_params is not None:
|
|
452
|
+
# Fallback to provided context_params (backward compatibility)
|
|
453
|
+
step_context_params = context_params
|
|
454
|
+
else:
|
|
455
|
+
step_context_params = {}
|
|
456
|
+
|
|
264
457
|
# Execute this step
|
|
265
458
|
result = self.execute_step(
|
|
266
459
|
step=step,
|
|
267
460
|
inputs=step_inputs,
|
|
268
|
-
context_params=
|
|
461
|
+
context_params=step_context_params,
|
|
269
462
|
cache_store=cache_store,
|
|
270
463
|
artifact_store=artifact_store,
|
|
271
464
|
run_id=run_id,
|