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
@@ -27,6 +27,7 @@ import { ProjectSelector } from './ProjectSelector';
27
27
 
28
28
  export function RunDetailsPanel({ run, onClose }) {
29
29
  const [details, setDetails] = useState(null);
30
+ const [artifacts, setArtifacts] = useState([]);
30
31
  const [loading, setLoading] = useState(false);
31
32
  const [activeTab, setActiveTab] = useState('overview'); // overview, steps, artifacts
32
33
  const [currentProject, setCurrentProject] = useState(run?.project);
@@ -41,9 +42,17 @@ export function RunDetailsPanel({ run, onClose }) {
41
42
  const fetchRunDetails = async () => {
42
43
  setLoading(true);
43
44
  try {
44
- const res = await fetchApi(`/api/runs/${run.run_id}`);
45
- const data = await res.json();
45
+ const [runRes, assetsRes] = await Promise.all([
46
+ fetchApi(`/api/runs/${run.run_id}`),
47
+ fetchApi(`/api/assets?run_id=${run.run_id}`)
48
+ ]);
49
+
50
+ const data = await runRes.json();
51
+ const assetsData = await assetsRes.json();
52
+
46
53
  setDetails(data);
54
+ setArtifacts(assetsData.assets || []);
55
+
47
56
  if (data.project) setCurrentProject(data.project);
48
57
  } catch (error) {
49
58
  console.error('Failed to fetch run details:', error);
@@ -177,11 +186,11 @@ export function RunDetailsPanel({ run, onClose }) {
177
186
  {activeTab === 'overview' && (
178
187
  <div className="space-y-4">
179
188
  {/* DAG Visualization Preview */}
180
- <Card className="p-0 overflow-hidden h-64 border-slate-200 dark:border-slate-700">
181
- <div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
189
+ <div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden h-[400px] flex flex-col bg-white dark:bg-slate-800">
190
+ <div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center shrink-0">
182
191
  <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Pipeline Graph</h3>
183
192
  </div>
184
- <div className="h-full bg-slate-50/50">
193
+ <div className="flex-1 min-h-0 bg-slate-50/50 dark:bg-slate-900/50">
185
194
  {runData.dag ? (
186
195
  <PipelineGraph
187
196
  dag={runData.dag}
@@ -193,7 +202,7 @@ export function RunDetailsPanel({ run, onClose }) {
193
202
  </div>
194
203
  )}
195
204
  </div>
196
- </Card>
205
+ </div>
197
206
 
198
207
  {/* Error Display if Failed */}
199
208
  {runData.status === 'failed' && runData.error && (
@@ -238,14 +247,33 @@ export function RunDetailsPanel({ run, onClose }) {
238
247
 
239
248
  {activeTab === 'artifacts' && (
240
249
  <div className="space-y-3">
241
- {/* This would need actual artifact data structure */}
242
- <div className="text-center py-8 text-slate-500">
243
- <Box size={32} className="mx-auto mb-2 opacity-50" />
244
- <p>Artifacts view not fully implemented in preview</p>
245
- <Link to={`/runs/${runData.run_id}`} className="text-primary-600 hover:underline text-sm mt-2 inline-block">
246
- View in full details page
247
- </Link>
248
- </div>
250
+ {artifacts.length > 0 ? (
251
+ artifacts.map(art => (
252
+ <div
253
+ key={art.artifact_id}
254
+ className="flex items-center gap-3 p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-primary-300 dark:hover:border-primary-700 transition-colors"
255
+ >
256
+ <div className="p-2 bg-slate-50 dark:bg-slate-700 rounded-md text-slate-500 dark:text-slate-400">
257
+ <FileText size={18} />
258
+ </div>
259
+ <div className="min-w-0 flex-1">
260
+ <p className="text-sm font-semibold text-slate-900 dark:text-white truncate">
261
+ {art.name}
262
+ </p>
263
+ <p className="text-xs text-slate-500 truncate">{art.type}</p>
264
+ </div>
265
+ <Link to={`/runs/${run.run_id}`}>
266
+ <Button variant="ghost" size="sm">
267
+ <ArrowRight size={14} />
268
+ </Button>
269
+ </Link>
270
+ </div>
271
+ ))
272
+ ) : (
273
+ <div className="text-center py-8 text-slate-500">
274
+ No artifacts found for this run.
275
+ </div>
276
+ )}
249
277
  </div>
250
278
  )}
251
279
  </>
@@ -16,6 +16,8 @@ import { Leaderboard } from '../app/leaderboard/page';
16
16
  import { Plugins } from '../app/plugins/page';
17
17
  import { Settings } from '../app/settings/page';
18
18
  import { TokenManagement } from '../app/tokens/page';
19
+ import { RunComparisonPage } from '../app/compare/page';
20
+ import { ExperimentComparisonPage } from '../app/experiments/compare/page';
19
21
 
20
22
  export const router = createBrowserRouter([
21
23
  {
@@ -25,9 +27,11 @@ export const router = createBrowserRouter([
25
27
  { index: true, element: <Dashboard /> },
26
28
  { path: 'pipelines', element: <Pipelines /> },
27
29
  { path: 'runs', element: <Runs /> },
30
+ { path: 'compare', element: <RunComparisonPage /> },
28
31
  { path: 'runs/:runId', element: <RunDetails /> },
29
32
  { path: 'assets', element: <Assets /> },
30
33
  { path: 'experiments', element: <Experiments /> },
34
+ { path: 'experiments/compare', element: <ExperimentComparisonPage /> },
31
35
  { path: 'experiments/:experimentId', element: <ExperimentDetails /> },
32
36
  { path: 'traces', element: <Traces /> },
33
37
  { path: 'projects', element: <Projects /> },
@@ -0,0 +1,181 @@
1
+ """UI Server Manager - Auto-start and manage the UI server in background."""
2
+
3
+ import threading
4
+ import time
5
+ import subprocess
6
+ from typing import Optional
7
+
8
+ from flowyml.ui.utils import is_ui_running, get_ui_url, get_ui_host_port
9
+
10
+
11
+ class UIServerManager:
12
+ """Manages the UI server lifecycle in background threads."""
13
+
14
+ _instance: Optional["UIServerManager"] = None
15
+ _lock = threading.Lock()
16
+
17
+ def __init__(self):
18
+ self._server_thread: Optional[threading.Thread] = None
19
+ self._server_process: Optional[subprocess.Popen] = None
20
+ # Initialize from config/env vars
21
+ self._host, self._port = get_ui_host_port()
22
+ self._running = False
23
+ self._started = False
24
+
25
+ @classmethod
26
+ def get_instance(cls) -> "UIServerManager":
27
+ """Get singleton instance of UI server manager."""
28
+ if cls._instance is None:
29
+ with cls._lock:
30
+ if cls._instance is None:
31
+ cls._instance = cls()
32
+ return cls._instance
33
+
34
+ def ensure_running(self, host: str | None = None, port: int | None = None, auto_start: bool = True) -> bool:
35
+ """Ensure UI server is running, start it if not and auto_start is True.
36
+
37
+ Args:
38
+ host: Host to bind to (uses config/env if None)
39
+ port: Port to bind to (uses config/env if None)
40
+ auto_start: If True, automatically start server if not running
41
+
42
+ Returns:
43
+ True if server is running, False otherwise
44
+ """
45
+ # Use provided values or get from config
46
+ if host is None or port is None:
47
+ config_host, config_port = get_ui_host_port()
48
+ self._host = host if host is not None else config_host
49
+ self._port = port if port is not None else config_port
50
+ else:
51
+ self._host = host
52
+ self._port = port
53
+
54
+ # Check if already running
55
+ if is_ui_running(host, port):
56
+ return True
57
+
58
+ if not auto_start:
59
+ return False
60
+
61
+ # Try to start the server
62
+ return self.start(host, port)
63
+
64
+ def start(self, host: str | None = None, port: int | None = None) -> bool:
65
+ """Start the UI server in a background thread.
66
+
67
+ Args:
68
+ host: Host to bind to (uses config/env if None)
69
+ port: Port to bind to (uses config/env if None)
70
+
71
+ Returns:
72
+ True if started successfully, False otherwise
73
+ """
74
+ # Use provided values or get from config
75
+ if host is None or port is None:
76
+ config_host, config_port = get_ui_host_port()
77
+ self._host = host if host is not None else config_host
78
+ self._port = port if port is not None else config_port
79
+ else:
80
+ self._host = host
81
+ self._port = port
82
+
83
+ if self._running:
84
+ return is_ui_running(self._host, self._port)
85
+
86
+ try:
87
+ # Check if UI dependencies are available
88
+ try:
89
+ import uvicorn
90
+
91
+ print(f"uvicorn {uvicorn.__version__}")
92
+ except ImportError:
93
+ return False
94
+
95
+ # Start server in a daemon thread
96
+ def run_server():
97
+ try:
98
+ import uvicorn
99
+
100
+ # Run uvicorn server (blocking call, but in daemon thread)
101
+ uvicorn.run(
102
+ "flowyml.ui.backend.main:app",
103
+ host=host,
104
+ port=port,
105
+ log_level="warning", # Reduce noise in background
106
+ access_log=False,
107
+ )
108
+ except Exception:
109
+ pass # Server will be stopped
110
+
111
+ # Start in daemon thread
112
+ self._server_thread = threading.Thread(
113
+ target=run_server,
114
+ daemon=True,
115
+ name="flowyml-ui-server",
116
+ )
117
+ self._server_thread.start()
118
+ self._running = True
119
+ self._started = True
120
+
121
+ # Wait a bit for server to start
122
+ max_wait = 5
123
+ for _ in range(max_wait * 10): # Check every 100ms
124
+ time.sleep(0.1)
125
+ if is_ui_running(host, port):
126
+ return True
127
+
128
+ # If we get here, server didn't start
129
+ self._running = False
130
+ return False
131
+
132
+ except Exception:
133
+ self._running = False
134
+ return False
135
+
136
+ def stop(self) -> None:
137
+ """Stop the UI server."""
138
+ # Since we're using a daemon thread, it will be killed when main process exits
139
+ # For now, we just mark it as stopped
140
+ self._running = False
141
+ self._server_thread = None
142
+
143
+ def get_url(self) -> Optional[str]:
144
+ """Get the URL of the running UI server.
145
+
146
+ Returns:
147
+ URL string if server is running, None otherwise
148
+ """
149
+ return get_ui_url(self._host, self._port)
150
+
151
+ def is_running(self) -> bool:
152
+ """Check if UI server is running."""
153
+ return is_ui_running(self._host, self._port)
154
+
155
+ def get_run_url(self, run_id: str) -> Optional[str]:
156
+ """Get URL to view a specific pipeline run.
157
+
158
+ Args:
159
+ run_id: ID of the pipeline run
160
+
161
+ Returns:
162
+ URL string if server is running, None otherwise
163
+ """
164
+ base_url = self.get_url()
165
+ if base_url:
166
+ return f"{base_url}/runs/{run_id}"
167
+ return None
168
+
169
+ def get_pipeline_url(self, pipeline_name: str) -> Optional[str]:
170
+ """Get URL to view a specific pipeline.
171
+
172
+ Args:
173
+ pipeline_name: Name of the pipeline
174
+
175
+ Returns:
176
+ URL string if server is running, None otherwise
177
+ """
178
+ base_url = self.get_url()
179
+ if base_url:
180
+ return f"{base_url}/pipelines/{pipeline_name}"
181
+ return None
flowyml/ui/utils.py CHANGED
@@ -1,8 +1,69 @@
1
1
  """UI utility functions for checking UI server status and getting URLs."""
2
2
 
3
+ import os
3
4
  import http.client
4
5
 
5
6
 
7
+ def get_ui_server_url() -> str:
8
+ """Get the UI server URL from configuration or environment variables.
9
+
10
+ Priority order:
11
+ 1. FLOWYML_SERVER_URL environment variable (explicit override)
12
+ 2. FLOWYML_REMOTE_UI_URL from config (for centralized deployments)
13
+ 3. FLOWYML_UI_HOST and FLOWYML_UI_PORT from config/env
14
+ 4. Default: http://localhost:8080
15
+
16
+ Returns:
17
+ Base URL of the UI server (e.g., "http://localhost:8080" or "https://flowyml.example.com")
18
+ """
19
+ # Check for explicit server URL override
20
+ server_url = os.getenv("FLOWYML_SERVER_URL")
21
+ if server_url:
22
+ return server_url.rstrip("/")
23
+
24
+ # Check for remote UI URL (centralized deployment)
25
+ try:
26
+ from flowyml.utils.config import get_config
27
+
28
+ config = get_config()
29
+ if config.remote_ui_url:
30
+ return config.remote_ui_url.rstrip("/")
31
+
32
+ # Use config values for host/port
33
+ host = os.getenv("FLOWYML_UI_HOST", config.ui_host)
34
+ port = int(os.getenv("FLOWYML_UI_PORT", str(config.ui_port)))
35
+
36
+ # Determine protocol based on port (443 = https, else http)
37
+ protocol = "https" if port == 443 else "http"
38
+
39
+ return f"{protocol}://{host}:{port}"
40
+ except Exception:
41
+ # Fallback to defaults
42
+ host = os.getenv("FLOWYML_UI_HOST", "localhost")
43
+ port = int(os.getenv("FLOWYML_UI_PORT", "8080"))
44
+ protocol = "https" if port == 443 else "http"
45
+ return f"{protocol}://{host}:{port}"
46
+
47
+
48
+ def get_ui_host_port() -> tuple[str, int]:
49
+ """Get UI host and port from configuration.
50
+
51
+ Returns:
52
+ Tuple of (host, port)
53
+ """
54
+ try:
55
+ from flowyml.utils.config import get_config
56
+
57
+ config = get_config()
58
+ host = os.getenv("FLOWYML_UI_HOST", config.ui_host)
59
+ port = int(os.getenv("FLOWYML_UI_PORT", str(config.ui_port)))
60
+ return (host, port)
61
+ except Exception:
62
+ host = os.getenv("FLOWYML_UI_HOST", "localhost")
63
+ port = int(os.getenv("FLOWYML_UI_PORT", "8080"))
64
+ return (host, port)
65
+
66
+
6
67
  def is_ui_running(host: str = "localhost", port: int = 8080) -> bool:
7
68
  """Check if the flowyml UI server is running.
8
69
 
@@ -39,7 +100,8 @@ def get_ui_url(host: str = "localhost", port: int = 8080) -> str | None:
39
100
  URL string if server is running, None otherwise
40
101
  """
41
102
  if is_ui_running(host, port):
42
- return f"http://{host}:{port}"
103
+ protocol = "https" if port == 443 else "http"
104
+ return f"{protocol}://{host}:{port}"
43
105
  return None
44
106
 
45
107
 
flowyml/utils/config.py CHANGED
@@ -27,9 +27,11 @@ class FlowymlConfig:
27
27
  remote_ui_url: str = ""
28
28
  remote_services: list[dict[str, str]] = field(default_factory=list)
29
29
  enable_caching: bool = True
30
+ enable_checkpointing: bool = True # Enable checkpointing by default
30
31
  enable_logging: bool = True
31
32
  log_level: str = "INFO"
32
33
  max_cache_size_mb: int = 10000 # 10GB default
34
+ checkpoint_dir: Path = field(default_factory=lambda: Path(".flowyml/checkpoints"))
33
35
 
34
36
  # UI settings
35
37
  ui_host: str = "localhost"
@@ -63,6 +65,7 @@ class FlowymlConfig:
63
65
  "runs_dir",
64
66
  "experiments_dir",
65
67
  "projects_dir",
68
+ "checkpoint_dir",
66
69
  ]:
67
70
  value = getattr(self, field_name)
68
71
  if not isinstance(value, Path):
@@ -76,6 +79,7 @@ class FlowymlConfig:
76
79
  self.runs_dir.mkdir(parents=True, exist_ok=True)
77
80
  self.experiments_dir.mkdir(parents=True, exist_ok=True)
78
81
  self.projects_dir.mkdir(parents=True, exist_ok=True)
82
+ self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
79
83
 
80
84
  # Create metadata db parent dir
81
85
  self.metadata_db.parent.mkdir(parents=True, exist_ok=True)
@@ -90,12 +94,14 @@ class FlowymlConfig:
90
94
  "runs_dir": str(self.runs_dir),
91
95
  "experiments_dir": str(self.experiments_dir),
92
96
  "projects_dir": str(self.projects_dir),
97
+ "checkpoint_dir": str(self.checkpoint_dir),
93
98
  "default_stack": self.default_stack,
94
99
  "execution_mode": self.execution_mode,
95
100
  "remote_server_url": self.remote_server_url,
96
101
  "remote_ui_url": self.remote_ui_url,
97
102
  "remote_services": self.remote_services,
98
103
  "enable_caching": self.enable_caching,
104
+ "enable_checkpointing": self.enable_checkpointing,
99
105
  "enable_logging": self.enable_logging,
100
106
  "log_level": self.log_level,
101
107
  "max_cache_size_mb": self.max_cache_size_mb,
@@ -274,6 +280,7 @@ def get_env_config() -> dict[str, Any]:
274
280
  "flowyml_EXECUTION_MODE": "execution_mode",
275
281
  "flowyml_REMOTE_SERVER_URL": "remote_server_url",
276
282
  "flowyml_REMOTE_UI_URL": "remote_ui_url",
283
+ "flowyml_SERVER_URL": "remote_ui_url", # Alias for FLOWYML_SERVER_URL -> remote_ui_url
277
284
  "flowyml_ENABLE_CACHING": "enable_caching",
278
285
  "flowyml_LOG_LEVEL": "log_level",
279
286
  "flowyml_UI_HOST": "ui_host",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowyml
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Next-Generation ML Pipeline Framework
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -23,13 +23,14 @@ Provides-Extra: aws
23
23
  Provides-Extra: azure
24
24
  Provides-Extra: gcp
25
25
  Provides-Extra: pytorch
26
+ Provides-Extra: rich
26
27
  Provides-Extra: sklearn
27
28
  Provides-Extra: tensorflow
28
29
  Provides-Extra: ui
29
30
  Requires-Dist: click (>=8.0.0)
30
31
  Requires-Dist: cloudpickle (>=2.0.0)
31
32
  Requires-Dist: croniter (>=2.0.1,<3.0.0)
32
- Requires-Dist: fastapi (>=0.122.0,<0.123.0) ; extra == "ui"
33
+ Requires-Dist: fastapi (>=0.122.0,<0.123.0) ; extra == "ui" or extra == "all"
33
34
  Requires-Dist: google-cloud-aiplatform (>=1.35.0) ; extra == "gcp" or extra == "all"
34
35
  Requires-Dist: google-cloud-storage (>=2.10.0) ; extra == "gcp" or extra == "all"
35
36
  Requires-Dist: httpx (>=0.24,<0.28)
@@ -41,6 +42,7 @@ Requires-Dist: pydantic (>=2.0.0)
41
42
  Requires-Dist: python-multipart (>=0.0.6) ; extra == "ui" or extra == "all"
42
43
  Requires-Dist: pytz (>=2024.1,<2025.0)
43
44
  Requires-Dist: pyyaml (>=6.0)
45
+ Requires-Dist: rich (>=13.0.0) ; extra == "rich"
44
46
  Requires-Dist: scikit-learn (>=1.0.0) ; extra == "sklearn" or extra == "all"
45
47
  Requires-Dist: sqlalchemy (>=2.0.0)
46
48
  Requires-Dist: tensorflow (>=2.12.0) ; extra == "tensorflow" or extra == "all"
@@ -254,7 +256,7 @@ pipeline.run(debug=True) # Pauses at breakpoint
254
256
  Assets are not just files; they are first-class citizens with lineage, metadata, and versioning.
255
257
 
256
258
  ```python
257
- from flowyml.core import Dataset, Model, Metrics, FeatureSet
259
+ from flowyml import Dataset, Model, Metrics, FeatureSet
258
260
 
259
261
  # Assets track their producer, lineage, and metadata automatically
260
262
  dataset = Dataset.create(data=df, name="training_data", metadata={"source": "s3"})