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.
Files changed (51) hide show
  1. flowyml/__init__.py +2 -1
  2. flowyml/assets/featureset.py +30 -5
  3. flowyml/assets/metrics.py +47 -4
  4. flowyml/cli/main.py +21 -0
  5. flowyml/cli/models.py +444 -0
  6. flowyml/cli/rich_utils.py +95 -0
  7. flowyml/core/checkpoint.py +6 -1
  8. flowyml/core/conditional.py +104 -0
  9. flowyml/core/display.py +525 -0
  10. flowyml/core/execution_status.py +1 -0
  11. flowyml/core/executor.py +201 -8
  12. flowyml/core/orchestrator.py +500 -7
  13. flowyml/core/pipeline.py +301 -11
  14. flowyml/core/project.py +4 -1
  15. flowyml/core/scheduler.py +225 -81
  16. flowyml/core/versioning.py +13 -4
  17. flowyml/registry/model_registry.py +1 -1
  18. flowyml/storage/sql.py +53 -13
  19. flowyml/ui/backend/main.py +2 -0
  20. flowyml/ui/backend/routers/assets.py +36 -0
  21. flowyml/ui/backend/routers/execution.py +2 -2
  22. flowyml/ui/backend/routers/runs.py +211 -0
  23. flowyml/ui/backend/routers/stats.py +2 -2
  24. flowyml/ui/backend/routers/websocket.py +121 -0
  25. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
  26. flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
  27. flowyml/ui/frontend/dist/index.html +2 -2
  28. flowyml/ui/frontend/package-lock.json +289 -0
  29. flowyml/ui/frontend/package.json +1 -0
  30. flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
  31. flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
  32. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
  33. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
  34. flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
  35. flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
  36. flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
  37. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
  38. flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
  39. flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
  40. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
  41. flowyml/ui/frontend/src/router/index.jsx +4 -0
  42. flowyml/ui/server_manager.py +181 -0
  43. flowyml/ui/utils.py +63 -1
  44. flowyml/utils/config.py +7 -0
  45. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
  46. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
  47. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
  48. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
  49. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
  50. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
  51. {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
- context_params: dict[str, Any],
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
- context_params: Parameters from context
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
- result = step.func(**kwargs)
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
- context_params: dict[str, Any],
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
- context_params: Parameters from context
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=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,