flowyml 1.2.0__py3-none-any.whl → 1.3.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 +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +22 -5
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +42 -4
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -2,6 +2,17 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { Search, LayoutGrid, List, Table as TableIcon, ArrowUpDown, Filter } from 'lucide-react';
|
|
3
3
|
import { Button } from './Button';
|
|
4
4
|
|
|
5
|
+
// Extracted TableRow component to avoid nested map minification issues
|
|
6
|
+
const TableRow = ({ item, columns }) => (
|
|
7
|
+
<tr className="bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
8
|
+
{columns.map((column, columnIndex) => (
|
|
9
|
+
<td key={columnIndex} className="px-6 py-4">
|
|
10
|
+
{column.render ? column.render(item) : item[column.key]}
|
|
11
|
+
</td>
|
|
12
|
+
))}
|
|
13
|
+
</tr>
|
|
14
|
+
);
|
|
15
|
+
|
|
5
16
|
export function DataView({
|
|
6
17
|
title,
|
|
7
18
|
subtitle,
|
|
@@ -11,7 +22,7 @@ export function DataView({
|
|
|
11
22
|
renderGrid,
|
|
12
23
|
renderList,
|
|
13
24
|
searchPlaceholder = "Search...",
|
|
14
|
-
initialView = '
|
|
25
|
+
initialView = 'table',
|
|
15
26
|
emptyState,
|
|
16
27
|
loading = false
|
|
17
28
|
}) {
|
|
@@ -19,8 +30,21 @@ export function DataView({
|
|
|
19
30
|
const [searchQuery, setSearchQuery] = useState('');
|
|
20
31
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
|
21
32
|
|
|
33
|
+
// Safety checks
|
|
34
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
35
|
+
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
36
|
+
|
|
37
|
+
// Default renderers if not provided
|
|
38
|
+
const defaultRenderGrid = (item) => (
|
|
39
|
+
<div className="p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
|
|
40
|
+
<pre className="text-xs overflow-auto">{JSON.stringify(item, null, 2)}</pre>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
const safeRenderGrid = renderGrid || defaultRenderGrid;
|
|
44
|
+
const safeRenderList = renderList || safeRenderGrid;
|
|
45
|
+
|
|
22
46
|
// Filter items
|
|
23
|
-
const filteredItems =
|
|
47
|
+
const filteredItems = safeItems.filter(item => {
|
|
24
48
|
if (!searchQuery) return true;
|
|
25
49
|
const searchStr = searchQuery.toLowerCase();
|
|
26
50
|
return Object.values(item).some(val =>
|
|
@@ -120,7 +144,7 @@ export function DataView({
|
|
|
120
144
|
{view === 'grid' && (
|
|
121
145
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
122
146
|
{sortedItems.map((item, idx) => (
|
|
123
|
-
<div key={idx}>{
|
|
147
|
+
<div key={idx}>{safeRenderGrid(item)}</div>
|
|
124
148
|
))}
|
|
125
149
|
</div>
|
|
126
150
|
)}
|
|
@@ -128,7 +152,7 @@ export function DataView({
|
|
|
128
152
|
{view === 'list' && (
|
|
129
153
|
<div className="space-y-4">
|
|
130
154
|
{sortedItems.map((item, idx) => (
|
|
131
|
-
<div key={idx}>{
|
|
155
|
+
<div key={idx}>{safeRenderList(item)}</div>
|
|
132
156
|
))}
|
|
133
157
|
</div>
|
|
134
158
|
)}
|
|
@@ -139,29 +163,23 @@ export function DataView({
|
|
|
139
163
|
<table className="w-full text-sm text-left">
|
|
140
164
|
<thead className="text-xs text-slate-500 uppercase bg-slate-50 dark:bg-slate-900/50 border-b border-slate-200 dark:border-slate-700">
|
|
141
165
|
<tr>
|
|
142
|
-
{
|
|
166
|
+
{safeColumns.map((column, columnIndex) => (
|
|
143
167
|
<th
|
|
144
|
-
key={
|
|
168
|
+
key={columnIndex}
|
|
145
169
|
className="px-6 py-3 font-medium cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
146
|
-
onClick={() =>
|
|
170
|
+
onClick={() => column.sortable && handleSort(column.key)}
|
|
147
171
|
>
|
|
148
172
|
<div className="flex items-center gap-2">
|
|
149
|
-
{
|
|
150
|
-
{
|
|
173
|
+
{column.header}
|
|
174
|
+
{column.sortable && <ArrowUpDown size={14} className="text-slate-400" />}
|
|
151
175
|
</div>
|
|
152
176
|
</th>
|
|
153
177
|
))}
|
|
154
178
|
</tr>
|
|
155
179
|
</thead>
|
|
156
180
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-700">
|
|
157
|
-
{sortedItems.map((item,
|
|
158
|
-
<
|
|
159
|
-
{columns.map((col, colIdx) => (
|
|
160
|
-
<td key={colIdx} className="px-6 py-4">
|
|
161
|
-
{col.render ? col.render(item) : item[col.key]}
|
|
162
|
-
</td>
|
|
163
|
-
))}
|
|
164
|
-
</tr>
|
|
181
|
+
{sortedItems.map((item, itemIndex) => (
|
|
182
|
+
<TableRow key={itemIndex} item={item} columns={safeColumns} />
|
|
165
183
|
))}
|
|
166
184
|
</tbody>
|
|
167
185
|
</table>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RefreshCw, Bug, Terminal } from 'lucide-react';
|
|
3
|
+
import { Button } from './Button';
|
|
4
|
+
|
|
5
|
+
export class ErrorBoundary extends React.Component {
|
|
6
|
+
constructor(props) {
|
|
7
|
+
super(props);
|
|
8
|
+
this.state = { hasError: false, error: null, errorInfo: null, reported: false };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getDerivedStateFromError(error) {
|
|
12
|
+
// Update state so the next render will show the fallback UI.
|
|
13
|
+
return { hasError: true, error };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
componentDidCatch(error, errorInfo) {
|
|
17
|
+
console.error("Uncaught error:", error, errorInfo);
|
|
18
|
+
this.setState({ errorInfo });
|
|
19
|
+
|
|
20
|
+
// Report to backend
|
|
21
|
+
this.reportError(error, errorInfo);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reportError = async (error, errorInfo) => {
|
|
25
|
+
if (this.state.reported) return;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Use relative URL to work with proxy or same origin
|
|
29
|
+
await fetch('/api/client/errors', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
message: error.message || 'Unknown error',
|
|
36
|
+
stack: error.stack,
|
|
37
|
+
component_stack: errorInfo?.componentStack,
|
|
38
|
+
url: window.location.href,
|
|
39
|
+
user_agent: navigator.userAgent
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
this.setState({ reported: true });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error("Failed to report error:", e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
handleReset = () => {
|
|
49
|
+
this.setState({ hasError: false, error: null, errorInfo: null, reported: false });
|
|
50
|
+
if (this.props.onReset) {
|
|
51
|
+
this.props.onReset();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
render() {
|
|
56
|
+
if (this.state.hasError) {
|
|
57
|
+
if (this.props.fallback) {
|
|
58
|
+
return this.props.fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="min-h-[400px] flex items-center justify-center p-6 w-full">
|
|
63
|
+
<div className="max-w-lg w-full bg-white dark:bg-gray-900 rounded-2xl shadow-xl overflow-hidden border border-gray-200 dark:border-gray-800">
|
|
64
|
+
<div className="bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500 h-2"></div>
|
|
65
|
+
<div className="p-8">
|
|
66
|
+
<div className="flex justify-center mb-6">
|
|
67
|
+
<div className="relative">
|
|
68
|
+
<div className="absolute inset-0 bg-red-100 dark:bg-red-900/30 rounded-full animate-ping opacity-75"></div>
|
|
69
|
+
<div className="relative p-4 bg-red-50 dark:bg-red-900/20 rounded-full">
|
|
70
|
+
<Bug className="w-12 h-12 text-red-500 dark:text-red-400" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<h2 className="text-2xl font-bold text-center text-gray-900 dark:text-white mb-2">
|
|
76
|
+
Well, this is awkward...
|
|
77
|
+
</h2>
|
|
78
|
+
|
|
79
|
+
<p className="text-center text-gray-600 dark:text-gray-400 mb-6">
|
|
80
|
+
The hamsters powering this component decided to take an unscheduled nap.
|
|
81
|
+
We've dispatched a team of digital veterinarians (aka developers) to wake them up.
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6 font-mono text-xs text-left overflow-auto max-h-32 border border-gray-200 dark:border-gray-700">
|
|
85
|
+
<div className="flex items-center text-red-500 mb-2">
|
|
86
|
+
<Terminal className="w-3 h-3 mr-2" />
|
|
87
|
+
<span className="font-semibold">Error Log</span>
|
|
88
|
+
</div>
|
|
89
|
+
<code className="text-gray-700 dark:text-gray-300 break-all whitespace-pre-wrap">
|
|
90
|
+
{this.state.error?.message || "Unknown error"}
|
|
91
|
+
</code>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div className="flex justify-center gap-4">
|
|
95
|
+
<Button
|
|
96
|
+
onClick={() => window.location.reload()}
|
|
97
|
+
variant="outline"
|
|
98
|
+
className="border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
99
|
+
>
|
|
100
|
+
Reload Page
|
|
101
|
+
</Button>
|
|
102
|
+
<Button
|
|
103
|
+
onClick={this.handleReset}
|
|
104
|
+
className="bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 text-white border-0"
|
|
105
|
+
>
|
|
106
|
+
<RefreshCw className="w-4 h-4 mr-2" />
|
|
107
|
+
Try Again
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.props.children;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -17,7 +17,7 @@ export function ProjectProvider({ children }) {
|
|
|
17
17
|
fetch('/api/projects/')
|
|
18
18
|
.then(res => res.json())
|
|
19
19
|
.then(data => {
|
|
20
|
-
setProjects(data || []);
|
|
20
|
+
setProjects(data.projects || []);
|
|
21
21
|
setLoading(false);
|
|
22
22
|
})
|
|
23
23
|
.catch(err => {
|
|
@@ -48,7 +48,7 @@ export function ProjectProvider({ children }) {
|
|
|
48
48
|
try {
|
|
49
49
|
const res = await fetch('/api/projects/');
|
|
50
50
|
const data = await res.json();
|
|
51
|
-
setProjects(data || []);
|
|
51
|
+
setProjects(data.projects || []);
|
|
52
52
|
} catch (err) {
|
|
53
53
|
console.error('Failed to refresh projects:', err);
|
|
54
54
|
} finally {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
2
|
+
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
|
|
5
|
+
const ToastContext = createContext();
|
|
6
|
+
|
|
7
|
+
export function ToastProvider({ children }) {
|
|
8
|
+
const [toasts, setToasts] = useState([]);
|
|
9
|
+
|
|
10
|
+
const addToast = useCallback((message, type = 'info', duration = 5000) => {
|
|
11
|
+
const id = Date.now() + Math.random();
|
|
12
|
+
setToasts(prev => [...prev, { id, message, type, duration }]);
|
|
13
|
+
|
|
14
|
+
if (duration > 0) {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
removeToast(id);
|
|
17
|
+
}, duration);
|
|
18
|
+
}
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const removeToast = useCallback((id) => {
|
|
22
|
+
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const success = useCallback((message, duration) => addToast(message, 'success', duration), [addToast]);
|
|
26
|
+
const error = useCallback((message, duration) => addToast(message, 'error', duration), [addToast]);
|
|
27
|
+
const warning = useCallback((message, duration) => addToast(message, 'warning', duration), [addToast]);
|
|
28
|
+
const info = useCallback((message, duration) => addToast(message, 'info', duration), [addToast]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<ToastContext.Provider value={{ success, error, warning, info, addToast }}>
|
|
32
|
+
{children}
|
|
33
|
+
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
|
34
|
+
</ToastContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useToast() {
|
|
39
|
+
const context = useContext(ToastContext);
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error('useToast must be used within ToastProvider');
|
|
42
|
+
}
|
|
43
|
+
return context;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ToastContainer({ toasts, onRemove }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
|
|
49
|
+
<AnimatePresence>
|
|
50
|
+
{toasts.map(toast => (
|
|
51
|
+
<Toast
|
|
52
|
+
key={toast.id}
|
|
53
|
+
toast={toast}
|
|
54
|
+
onRemove={() => onRemove(toast.id)}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</AnimatePresence>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Toast({ toast, onRemove }) {
|
|
63
|
+
const { message, type } = toast;
|
|
64
|
+
|
|
65
|
+
const config = {
|
|
66
|
+
success: {
|
|
67
|
+
icon: CheckCircle,
|
|
68
|
+
bgClass: 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800',
|
|
69
|
+
iconClass: 'text-emerald-600 dark:text-emerald-400',
|
|
70
|
+
textClass: 'text-emerald-900 dark:text-emerald-100'
|
|
71
|
+
},
|
|
72
|
+
error: {
|
|
73
|
+
icon: AlertCircle,
|
|
74
|
+
bgClass: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800',
|
|
75
|
+
iconClass: 'text-red-600 dark:text-red-400',
|
|
76
|
+
textClass: 'text-red-900 dark:text-red-100'
|
|
77
|
+
},
|
|
78
|
+
warning: {
|
|
79
|
+
icon: AlertTriangle,
|
|
80
|
+
bgClass: 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800',
|
|
81
|
+
iconClass: 'text-amber-600 dark:text-amber-400',
|
|
82
|
+
textClass: 'text-amber-900 dark:text-amber-100'
|
|
83
|
+
},
|
|
84
|
+
info: {
|
|
85
|
+
icon: Info,
|
|
86
|
+
bgClass: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
|
|
87
|
+
iconClass: 'text-blue-600 dark:text-blue-400',
|
|
88
|
+
textClass: 'text-blue-900 dark:text-blue-100'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const { icon: Icon, bgClass, iconClass, textClass } = config[type] || config.info;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<motion.div
|
|
96
|
+
initial={{ opacity: 0, y: -20, scale: 0.95 }}
|
|
97
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
98
|
+
exit={{ opacity: 0, x: 100, scale: 0.95 }}
|
|
99
|
+
transition={{ duration: 0.2 }}
|
|
100
|
+
className={`pointer-events-auto ${bgClass} border rounded-lg shadow-lg p-4 pr-12 max-w-md relative`}
|
|
101
|
+
>
|
|
102
|
+
<div className="flex items-start gap-3">
|
|
103
|
+
<Icon className={`${iconClass} shrink-0 mt-0.5`} size={20} />
|
|
104
|
+
<p className={`${textClass} text-sm font-medium leading-relaxed`}>
|
|
105
|
+
{message}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<button
|
|
109
|
+
onClick={onRemove}
|
|
110
|
+
className={`absolute top-3 right-3 ${iconClass} hover:opacity-70 transition-opacity`}
|
|
111
|
+
>
|
|
112
|
+
<X size={16} />
|
|
113
|
+
</button>
|
|
114
|
+
</motion.div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -3,6 +3,8 @@ import { Outlet } from 'react-router-dom';
|
|
|
3
3
|
import { Sidebar } from '../components/sidebar/Sidebar';
|
|
4
4
|
import { Header } from '../components/header/Header';
|
|
5
5
|
|
|
6
|
+
import { ErrorBoundary } from '../components/ui/ErrorBoundary';
|
|
7
|
+
|
|
6
8
|
export function MainLayout() {
|
|
7
9
|
const [collapsed, setCollapsed] = useState(false);
|
|
8
10
|
|
|
@@ -14,7 +16,9 @@ export function MainLayout() {
|
|
|
14
16
|
<Header />
|
|
15
17
|
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700">
|
|
16
18
|
<div className="max-w-7xl mx-auto w-full">
|
|
17
|
-
<
|
|
19
|
+
<ErrorBoundary>
|
|
20
|
+
<Outlet />
|
|
21
|
+
</ErrorBoundary>
|
|
18
22
|
</div>
|
|
19
23
|
</main>
|
|
20
24
|
</div>
|
|
@@ -9,7 +9,9 @@ import { Experiments } from '../app/experiments/page';
|
|
|
9
9
|
import { ExperimentDetails } from '../app/experiments/[experimentId]/page';
|
|
10
10
|
import { Traces } from '../app/traces/page';
|
|
11
11
|
import { Projects } from '../app/projects/page';
|
|
12
|
+
import { ProjectDetails } from '../app/projects/[projectId]/page';
|
|
12
13
|
import { Schedules } from '../app/schedules/page';
|
|
14
|
+
import { Observability } from '../app/observability/page';
|
|
13
15
|
import { Leaderboard } from '../app/leaderboard/page';
|
|
14
16
|
import { Plugins } from '../app/plugins/page';
|
|
15
17
|
import { Settings } from '../app/settings/page';
|
|
@@ -29,7 +31,9 @@ export const router = createBrowserRouter([
|
|
|
29
31
|
{ path: 'experiments/:experimentId', element: <ExperimentDetails /> },
|
|
30
32
|
{ path: 'traces', element: <Traces /> },
|
|
31
33
|
{ path: 'projects', element: <Projects /> },
|
|
34
|
+
{ path: 'projects/:projectId', element: <ProjectDetails /> },
|
|
32
35
|
{ path: 'schedules', element: <Schedules /> },
|
|
36
|
+
{ path: 'observability', element: <Observability /> },
|
|
33
37
|
{ path: 'leaderboard', element: <Leaderboard /> },
|
|
34
38
|
{ path: 'plugins', element: <Plugins /> },
|
|
35
39
|
{ path: 'settings', element: <Settings /> },
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getBaseUrl } from './api';
|
|
2
|
+
|
|
3
|
+
export const downloadArtifactById = async (artifactId) => {
|
|
4
|
+
if (!artifactId) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const baseUrl = await getBaseUrl();
|
|
9
|
+
const url = `${baseUrl}/api/assets/${artifactId}/download`;
|
|
10
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
11
|
+
};
|
flowyml/utils/config.py
CHANGED
|
@@ -18,12 +18,14 @@ class FlowymlConfig:
|
|
|
18
18
|
cache_dir: Path = field(default_factory=lambda: Path(".flowyml/cache"))
|
|
19
19
|
runs_dir: Path = field(default_factory=lambda: Path(".flowyml/runs"))
|
|
20
20
|
experiments_dir: Path = field(default_factory=lambda: Path(".flowyml/experiments"))
|
|
21
|
+
projects_dir: Path = field(default_factory=lambda: Path(".flowyml/projects"))
|
|
21
22
|
|
|
22
23
|
# Execution settings
|
|
23
24
|
default_stack: str = "local"
|
|
24
25
|
execution_mode: str = "local" # local or remote
|
|
25
26
|
remote_server_url: str = ""
|
|
26
27
|
remote_ui_url: str = ""
|
|
28
|
+
remote_services: list[dict[str, str]] = field(default_factory=list)
|
|
27
29
|
enable_caching: bool = True
|
|
28
30
|
enable_logging: bool = True
|
|
29
31
|
log_level: str = "INFO"
|
|
@@ -60,6 +62,7 @@ class FlowymlConfig:
|
|
|
60
62
|
"cache_dir",
|
|
61
63
|
"runs_dir",
|
|
62
64
|
"experiments_dir",
|
|
65
|
+
"projects_dir",
|
|
63
66
|
]:
|
|
64
67
|
value = getattr(self, field_name)
|
|
65
68
|
if not isinstance(value, Path):
|
|
@@ -72,6 +75,7 @@ class FlowymlConfig:
|
|
|
72
75
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
73
76
|
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
74
77
|
self.experiments_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
self.projects_dir.mkdir(parents=True, exist_ok=True)
|
|
75
79
|
|
|
76
80
|
# Create metadata db parent dir
|
|
77
81
|
self.metadata_db.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -85,10 +89,12 @@ class FlowymlConfig:
|
|
|
85
89
|
"cache_dir": str(self.cache_dir),
|
|
86
90
|
"runs_dir": str(self.runs_dir),
|
|
87
91
|
"experiments_dir": str(self.experiments_dir),
|
|
92
|
+
"projects_dir": str(self.projects_dir),
|
|
88
93
|
"default_stack": self.default_stack,
|
|
89
94
|
"execution_mode": self.execution_mode,
|
|
90
95
|
"remote_server_url": self.remote_server_url,
|
|
91
96
|
"remote_ui_url": self.remote_ui_url,
|
|
97
|
+
"remote_services": self.remote_services,
|
|
92
98
|
"enable_caching": self.enable_caching,
|
|
93
99
|
"enable_logging": self.enable_logging,
|
|
94
100
|
"log_level": self.log_level,
|
flowyml/utils/stack_config.py
CHANGED
|
@@ -232,13 +232,55 @@ def create_stack_from_config(config: dict[str, Any], name: str):
|
|
|
232
232
|
elif stack_type == "gcp":
|
|
233
233
|
from flowyml.stacks.gcp import GCPStack
|
|
234
234
|
|
|
235
|
+
artifact_cfg = config.get("artifact_store", {})
|
|
236
|
+
registry_cfg = config.get("container_registry", {})
|
|
237
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
238
|
+
|
|
235
239
|
return GCPStack(
|
|
236
240
|
name=name,
|
|
237
241
|
project_id=config.get("project_id"),
|
|
238
242
|
region=config.get("region", "us-central1"),
|
|
239
|
-
bucket_name=
|
|
240
|
-
registry_uri=
|
|
241
|
-
service_account=
|
|
243
|
+
bucket_name=artifact_cfg.get("bucket"),
|
|
244
|
+
registry_uri=registry_cfg.get("uri"),
|
|
245
|
+
service_account=orchestrator_cfg.get("service_account"),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
elif stack_type == "aws":
|
|
249
|
+
from flowyml.stacks.aws import AWSStack
|
|
250
|
+
|
|
251
|
+
artifact_cfg = config.get("artifact_store", {})
|
|
252
|
+
registry_cfg = config.get("container_registry", {})
|
|
253
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
254
|
+
|
|
255
|
+
return AWSStack(
|
|
256
|
+
name=name,
|
|
257
|
+
region=config.get("region", "us-east-1"),
|
|
258
|
+
bucket_name=artifact_cfg.get("bucket"),
|
|
259
|
+
account_id=registry_cfg.get("account_id"),
|
|
260
|
+
registry_alias=registry_cfg.get("registry_alias"),
|
|
261
|
+
job_queue=orchestrator_cfg.get("job_queue"),
|
|
262
|
+
job_definition=orchestrator_cfg.get("job_definition"),
|
|
263
|
+
orchestrator_type=orchestrator_cfg.get("type", "batch"),
|
|
264
|
+
role_arn=orchestrator_cfg.get("role_arn"),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
elif stack_type == "azure":
|
|
268
|
+
from flowyml.stacks.azure import AzureMLStack
|
|
269
|
+
|
|
270
|
+
artifact_cfg = config.get("artifact_store", {})
|
|
271
|
+
registry_cfg = config.get("container_registry", {})
|
|
272
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
273
|
+
|
|
274
|
+
return AzureMLStack(
|
|
275
|
+
name=name,
|
|
276
|
+
subscription_id=config.get("subscription_id"),
|
|
277
|
+
resource_group=config.get("resource_group"),
|
|
278
|
+
workspace_name=config.get("workspace_name"),
|
|
279
|
+
compute=orchestrator_cfg.get("compute"),
|
|
280
|
+
account_url=artifact_cfg.get("account_url"),
|
|
281
|
+
container_name=artifact_cfg.get("container"),
|
|
282
|
+
registry_name=registry_cfg.get("name"),
|
|
283
|
+
login_server=registry_cfg.get("login_server"),
|
|
242
284
|
)
|
|
243
285
|
|
|
244
286
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowyml
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Next-Generation ML Pipeline Framework
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,16 +19,20 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Provides-Extra: all
|
|
22
|
+
Provides-Extra: aws
|
|
23
|
+
Provides-Extra: azure
|
|
22
24
|
Provides-Extra: gcp
|
|
23
25
|
Provides-Extra: pytorch
|
|
24
26
|
Provides-Extra: sklearn
|
|
25
27
|
Provides-Extra: tensorflow
|
|
26
28
|
Provides-Extra: ui
|
|
27
29
|
Requires-Dist: click (>=8.0.0)
|
|
30
|
+
Requires-Dist: cloudpickle (>=2.0.0)
|
|
28
31
|
Requires-Dist: croniter (>=2.0.1,<3.0.0)
|
|
29
32
|
Requires-Dist: fastapi (>=0.122.0,<0.123.0) ; extra == "ui"
|
|
30
33
|
Requires-Dist: google-cloud-aiplatform (>=1.35.0) ; extra == "gcp" or extra == "all"
|
|
31
34
|
Requires-Dist: google-cloud-storage (>=2.10.0) ; extra == "gcp" or extra == "all"
|
|
35
|
+
Requires-Dist: httpx (>=0.24,<0.28)
|
|
32
36
|
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
33
37
|
Requires-Dist: numpy (>=1.20.0)
|
|
34
38
|
Requires-Dist: pandas (>=1.3.0)
|
|
@@ -304,7 +308,41 @@ Ready for the enterprise. Run locally per project or deploy as a centralized ent
|
|
|
304
308
|
- **Cloud Providers**: AWS, GCP, Azure (via plugins).
|
|
305
309
|
- **Tools**: MLflow, Weights & Biases, Great Expectations.
|
|
306
310
|
|
|
307
|
-
### 20.
|
|
311
|
+
### 20. 📊 Automatic Training History Tracking
|
|
312
|
+
FlowyML automatically captures and visualizes your model training progress with zero manual intervention.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from flowyml.integrations.keras import FlowymlKerasCallback
|
|
316
|
+
|
|
317
|
+
# Just add the callback - that's it!
|
|
318
|
+
callback = FlowymlKerasCallback(
|
|
319
|
+
experiment_name="my-experiment",
|
|
320
|
+
project="my-project"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
model.fit(
|
|
324
|
+
x_train, y_train,
|
|
325
|
+
validation_data=(x_val, y_val),
|
|
326
|
+
epochs=50,
|
|
327
|
+
callbacks=[callback] # Auto-tracks all metrics!
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**What gets captured automatically:**
|
|
332
|
+
- ✅ Loss (train & validation) per epoch
|
|
333
|
+
- ✅ Accuracy (train & validation) per epoch
|
|
334
|
+
- ✅ All custom metrics (F1, precision, recall, etc.)
|
|
335
|
+
- ✅ Model architecture and parameters
|
|
336
|
+
- ✅ Interactive charts in the UI
|
|
337
|
+
|
|
338
|
+
**View beautiful training graphs in the UI:**
|
|
339
|
+
1. Navigate to your project's Structure tab
|
|
340
|
+
2. Click on any model artifact
|
|
341
|
+
3. See interactive loss/accuracy charts over epochs!
|
|
342
|
+
|
|
343
|
+
No external tools needed - all visualization built into FlowyML.
|
|
344
|
+
|
|
345
|
+
### 21. 📂 Project-Based Organization
|
|
308
346
|
Built-in multi-tenancy for managing multiple teams and initiatives.
|
|
309
347
|
|
|
310
348
|
```python
|
|
@@ -318,7 +356,7 @@ runs = project.list_runs()
|
|
|
318
356
|
stats = project.get_stats()
|
|
319
357
|
```
|
|
320
358
|
|
|
321
|
-
###
|
|
359
|
+
### 22. 📝 Pipeline Templates
|
|
322
360
|
Stop reinventing the wheel. Use pre-built templates for common ML patterns.
|
|
323
361
|
|
|
324
362
|
```python
|
|
@@ -382,7 +420,7 @@ print(f"Run ID: {result.run_id}")
|
|
|
382
420
|
print(f"Model Score: {result.outputs['model'].score}")
|
|
383
421
|
```
|
|
384
422
|
|
|
385
|
-
###
|
|
423
|
+
### 23. 🌐 Pipeline Versioning
|
|
386
424
|
Git-like versioning for pipelines. Track changes, compare, rollback.
|
|
387
425
|
|
|
388
426
|
```python
|