flowyml 1.1.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.
Files changed (92) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +22 -5
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/step.py +18 -1
  16. flowyml/core/submission_result.py +53 -0
  17. flowyml/core/versioning.py +2 -2
  18. flowyml/integrations/keras.py +95 -22
  19. flowyml/monitoring/alerts.py +2 -2
  20. flowyml/stacks/__init__.py +15 -0
  21. flowyml/stacks/aws.py +599 -0
  22. flowyml/stacks/azure.py +295 -0
  23. flowyml/stacks/components.py +24 -2
  24. flowyml/stacks/gcp.py +158 -11
  25. flowyml/stacks/local.py +5 -0
  26. flowyml/storage/artifacts.py +15 -5
  27. flowyml/storage/materializers/__init__.py +2 -0
  28. flowyml/storage/materializers/cloudpickle.py +74 -0
  29. flowyml/storage/metadata.py +166 -5
  30. flowyml/ui/backend/main.py +41 -1
  31. flowyml/ui/backend/routers/assets.py +356 -15
  32. flowyml/ui/backend/routers/client.py +46 -0
  33. flowyml/ui/backend/routers/execution.py +13 -2
  34. flowyml/ui/backend/routers/experiments.py +48 -12
  35. flowyml/ui/backend/routers/metrics.py +213 -0
  36. flowyml/ui/backend/routers/pipelines.py +63 -7
  37. flowyml/ui/backend/routers/projects.py +33 -7
  38. flowyml/ui/backend/routers/runs.py +150 -8
  39. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  40. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  41. flowyml/ui/frontend/dist/index.html +2 -2
  42. flowyml/ui/frontend/src/App.jsx +4 -1
  43. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  44. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  45. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  46. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  47. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  48. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  49. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  50. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  51. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  52. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  53. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  54. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  55. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  56. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  57. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  58. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  59. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  60. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  61. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  62. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  63. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  64. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  65. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  66. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  67. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  68. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  69. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  70. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  71. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  72. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  73. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  74. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  75. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  76. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  77. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  78. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  79. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  80. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  81. flowyml/ui/frontend/src/router/index.jsx +4 -0
  82. flowyml/ui/frontend/src/utils/date.js +10 -0
  83. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  84. flowyml/utils/config.py +6 -0
  85. flowyml/utils/stack_config.py +45 -3
  86. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
  87. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
  88. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
  89. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  90. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  91. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
  92. {flowyml-1.1.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 = 'grid',
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 = items.filter(item => {
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}>{renderGrid(item)}</div>
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}>{renderList ? renderList(item) : renderGrid(item)}</div>
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
- {columns.map((col, idx) => (
166
+ {safeColumns.map((column, columnIndex) => (
143
167
  <th
144
- key={idx}
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={() => col.sortable && handleSort(col.key)}
170
+ onClick={() => column.sortable && handleSort(column.key)}
147
171
  >
148
172
  <div className="flex items-center gap-2">
149
- {col.header}
150
- {col.sortable && <ArrowUpDown size={14} className="text-slate-400" />}
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, idx) => (
158
- <tr key={idx} className="bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
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
- <Outlet />
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,10 @@
1
+ import { format, isValid } from 'date-fns';
2
+
3
+ export function formatDate(dateString, formatStr = 'MMM d, yyyy') {
4
+ if (!dateString) return '-';
5
+
6
+ const date = new Date(dateString);
7
+ if (!isValid(date)) return '-';
8
+
9
+ return format(date, formatStr);
10
+ }
@@ -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,
@@ -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=config.get("artifact_store", {}).get("bucket"),
240
- registry_uri=config.get("container_registry", {}).get("uri"),
241
- service_account=config.get("orchestrator", {}).get("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: