flowyml 1.2.0__py3-none-any.whl → 1.4.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 (104) 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 +34 -17
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/scheduler.py +9 -9
  16. flowyml/core/scheduler_config.py +2 -3
  17. flowyml/core/step.py +18 -1
  18. flowyml/core/submission_result.py +53 -0
  19. flowyml/integrations/keras.py +95 -22
  20. flowyml/monitoring/alerts.py +2 -2
  21. flowyml/stacks/__init__.py +15 -0
  22. flowyml/stacks/aws.py +599 -0
  23. flowyml/stacks/azure.py +295 -0
  24. flowyml/stacks/bridge.py +9 -9
  25. flowyml/stacks/components.py +24 -2
  26. flowyml/stacks/gcp.py +158 -11
  27. flowyml/stacks/local.py +5 -0
  28. flowyml/stacks/plugins.py +2 -2
  29. flowyml/stacks/registry.py +21 -0
  30. flowyml/storage/artifacts.py +15 -5
  31. flowyml/storage/materializers/__init__.py +2 -0
  32. flowyml/storage/materializers/base.py +33 -0
  33. flowyml/storage/materializers/cloudpickle.py +74 -0
  34. flowyml/storage/metadata.py +3 -881
  35. flowyml/storage/remote.py +590 -0
  36. flowyml/storage/sql.py +911 -0
  37. flowyml/ui/backend/dependencies.py +28 -0
  38. flowyml/ui/backend/main.py +43 -80
  39. flowyml/ui/backend/routers/assets.py +483 -17
  40. flowyml/ui/backend/routers/client.py +46 -0
  41. flowyml/ui/backend/routers/execution.py +13 -2
  42. flowyml/ui/backend/routers/experiments.py +97 -14
  43. flowyml/ui/backend/routers/metrics.py +168 -0
  44. flowyml/ui/backend/routers/pipelines.py +77 -12
  45. flowyml/ui/backend/routers/projects.py +33 -7
  46. flowyml/ui/backend/routers/runs.py +221 -12
  47. flowyml/ui/backend/routers/schedules.py +5 -21
  48. flowyml/ui/backend/routers/stats.py +14 -0
  49. flowyml/ui/backend/routers/traces.py +37 -53
  50. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  51. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  52. flowyml/ui/frontend/dist/index.html +2 -2
  53. flowyml/ui/frontend/src/App.jsx +4 -1
  54. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  55. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  56. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  57. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  58. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  59. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  60. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  61. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  62. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  63. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  64. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  65. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  66. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  67. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  68. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  69. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  70. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  71. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  72. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  73. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  74. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  75. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  76. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  77. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  78. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  79. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  80. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  81. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  82. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  83. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  84. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  85. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  86. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  87. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  88. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  89. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  90. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  91. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  92. flowyml/ui/frontend/src/router/index.jsx +4 -0
  93. flowyml/ui/frontend/src/utils/date.js +10 -0
  94. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  95. flowyml/utils/config.py +6 -0
  96. flowyml/utils/stack_config.py +45 -3
  97. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
  98. flowyml-1.4.0.dist-info/RECORD +200 -0
  99. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
  100. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  101. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  102. flowyml-1.2.0.dist-info/RECORD +0 -159
  103. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
  104. {flowyml-1.2.0.dist-info → flowyml-1.4.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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowyml
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Next-Generation ML Pipeline Framework
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -19,24 +19,30 @@ 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)
39
+ Requires-Dist: psycopg2-binary (>=2.9.0)
35
40
  Requires-Dist: pydantic (>=2.0.0)
36
41
  Requires-Dist: python-multipart (>=0.0.6) ; extra == "ui" or extra == "all"
37
42
  Requires-Dist: pytz (>=2024.1,<2025.0)
38
43
  Requires-Dist: pyyaml (>=6.0)
39
44
  Requires-Dist: scikit-learn (>=1.0.0) ; extra == "sklearn" or extra == "all"
45
+ Requires-Dist: sqlalchemy (>=2.0.0)
40
46
  Requires-Dist: tensorflow (>=2.12.0) ; extra == "tensorflow" or extra == "all"
41
47
  Requires-Dist: toml (>=0.10.2)
42
48
  Requires-Dist: torch (>=2.0.0) ; extra == "pytorch" or extra == "all"
@@ -304,7 +310,41 @@ Ready for the enterprise. Run locally per project or deploy as a centralized ent
304
310
  - **Cloud Providers**: AWS, GCP, Azure (via plugins).
305
311
  - **Tools**: MLflow, Weights & Biases, Great Expectations.
306
312
 
307
- ### 20. 📂 Project-Based Organization
313
+ ### 20. 📊 Automatic Training History Tracking
314
+ FlowyML automatically captures and visualizes your model training progress with zero manual intervention.
315
+
316
+ ```python
317
+ from flowyml.integrations.keras import FlowymlKerasCallback
318
+
319
+ # Just add the callback - that's it!
320
+ callback = FlowymlKerasCallback(
321
+ experiment_name="my-experiment",
322
+ project="my-project"
323
+ )
324
+
325
+ model.fit(
326
+ x_train, y_train,
327
+ validation_data=(x_val, y_val),
328
+ epochs=50,
329
+ callbacks=[callback] # Auto-tracks all metrics!
330
+ )
331
+ ```
332
+
333
+ **What gets captured automatically:**
334
+ - ✅ Loss (train & validation) per epoch
335
+ - ✅ Accuracy (train & validation) per epoch
336
+ - ✅ All custom metrics (F1, precision, recall, etc.)
337
+ - ✅ Model architecture and parameters
338
+ - ✅ Interactive charts in the UI
339
+
340
+ **View beautiful training graphs in the UI:**
341
+ 1. Navigate to your project's Structure tab
342
+ 2. Click on any model artifact
343
+ 3. See interactive loss/accuracy charts over epochs!
344
+
345
+ No external tools needed - all visualization built into FlowyML.
346
+
347
+ ### 21. 📂 Project-Based Organization
308
348
  Built-in multi-tenancy for managing multiple teams and initiatives.
309
349
 
310
350
  ```python
@@ -318,7 +358,7 @@ runs = project.list_runs()
318
358
  stats = project.get_stats()
319
359
  ```
320
360
 
321
- ### 21. 📝 Pipeline Templates
361
+ ### 22. 📝 Pipeline Templates
322
362
  Stop reinventing the wheel. Use pre-built templates for common ML patterns.
323
363
 
324
364
  ```python
@@ -382,7 +422,7 @@ print(f"Run ID: {result.run_id}")
382
422
  print(f"Model Score: {result.outputs['model'].score}")
383
423
  ```
384
424
 
385
- ### 16. 🌐 Pipeline Versioning
425
+ ### 23. 🌐 Pipeline Versioning
386
426
  Git-like versioning for pipelines. Track changes, compare, rollback.
387
427
 
388
428
  ```python