flowyml 1.7.1__py3-none-any.whl → 1.8.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/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,30 +18,57 @@ import { Settings } from '../app/settings/page';
|
|
|
18
18
|
import { TokenManagement } from '../app/tokens/page';
|
|
19
19
|
import { RunComparisonPage } from '../app/compare/page';
|
|
20
20
|
import { ExperimentComparisonPage } from '../app/experiments/compare/page';
|
|
21
|
+
import { DeploymentLab } from '../app/deployments/page';
|
|
22
|
+
import { ModelExplorer } from '../app/model-explorer/page';
|
|
23
|
+
import { Login } from '../app/auth/Login';
|
|
24
|
+
import { RequireAuth, AuthProvider } from '../contexts/AuthContext';
|
|
25
|
+
import { Outlet } from 'react-router-dom';
|
|
26
|
+
|
|
27
|
+
// Layout Wrapper to provide Auth Context
|
|
28
|
+
const AppLayout = () => (
|
|
29
|
+
<AuthProvider>
|
|
30
|
+
<Outlet />
|
|
31
|
+
</AuthProvider>
|
|
32
|
+
);
|
|
21
33
|
|
|
22
34
|
export const router = createBrowserRouter([
|
|
23
35
|
{
|
|
24
|
-
|
|
25
|
-
element: <MainLayout />,
|
|
36
|
+
element: <AppLayout />, // Wrap everything in AuthProvider
|
|
26
37
|
children: [
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
{
|
|
39
|
+
path: '/login',
|
|
40
|
+
element: <Login />,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: '/',
|
|
44
|
+
element: (
|
|
45
|
+
<RequireAuth>
|
|
46
|
+
<MainLayout />
|
|
47
|
+
</RequireAuth>
|
|
48
|
+
),
|
|
49
|
+
children: [
|
|
50
|
+
{ index: true, element: <Dashboard /> },
|
|
51
|
+
{ path: 'pipelines', element: <Pipelines /> },
|
|
52
|
+
{ path: 'runs', element: <Runs /> },
|
|
53
|
+
{ path: 'compare', element: <RunComparisonPage /> },
|
|
54
|
+
{ path: 'runs/:runId', element: <RunDetails /> },
|
|
55
|
+
{ path: 'assets', element: <Assets /> },
|
|
56
|
+
{ path: 'experiments', element: <Experiments /> },
|
|
57
|
+
{ path: 'experiments/compare', element: <ExperimentComparisonPage /> },
|
|
58
|
+
{ path: 'experiments/:experimentId', element: <ExperimentDetails /> },
|
|
59
|
+
{ path: 'traces', element: <Traces /> },
|
|
60
|
+
{ path: 'projects', element: <Projects /> },
|
|
61
|
+
{ path: 'projects/:projectId', element: <ProjectDetails /> },
|
|
62
|
+
{ path: 'schedules', element: <Schedules /> },
|
|
63
|
+
{ path: 'observability', element: <Observability /> },
|
|
64
|
+
{ path: 'leaderboard', element: <Leaderboard /> },
|
|
65
|
+
{ path: 'plugins', element: <Plugins /> },
|
|
66
|
+
{ path: 'settings', element: <Settings /> },
|
|
67
|
+
{ path: 'tokens', element: <TokenManagement /> },
|
|
68
|
+
{ path: 'deployments', element: <DeploymentLab /> },
|
|
69
|
+
{ path: 'model-explorer', element: <ModelExplorer /> },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
45
72
|
],
|
|
46
73
|
},
|
|
47
74
|
]);
|
|
@@ -66,8 +66,10 @@ class PluginService {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
async
|
|
69
|
+
async importStack(stackName, type = 'zenml') {
|
|
70
70
|
try {
|
|
71
|
+
// currently backend only supports zenml import at this endpoint
|
|
72
|
+
// in the future we can add ?type= param or body field
|
|
71
73
|
const response = await fetch(`${API_BASE_URL}/plugins/import-stack`, {
|
|
72
74
|
method: 'POST',
|
|
73
75
|
headers: { 'Content-Type': 'application/json' },
|
flowyml/ui/server_manager.py
CHANGED
|
@@ -15,8 +15,8 @@ class UIServerManager:
|
|
|
15
15
|
_lock = threading.Lock()
|
|
16
16
|
|
|
17
17
|
def __init__(self):
|
|
18
|
-
self._server_thread:
|
|
19
|
-
self._server_process:
|
|
18
|
+
self._server_thread: threading.Thread | None = None
|
|
19
|
+
self._server_process: subprocess.Popen | None = None
|
|
20
20
|
# Initialize from config/env vars
|
|
21
21
|
self._host, self._port = get_ui_host_port()
|
|
22
22
|
self._running = False
|
|
@@ -148,7 +148,7 @@ class UIServerManager:
|
|
|
148
148
|
self._running = False
|
|
149
149
|
self._server_thread = None
|
|
150
150
|
|
|
151
|
-
def get_url(self) ->
|
|
151
|
+
def get_url(self) -> str | None:
|
|
152
152
|
"""Get the URL of the running UI server.
|
|
153
153
|
|
|
154
154
|
Returns:
|
|
@@ -160,7 +160,7 @@ class UIServerManager:
|
|
|
160
160
|
"""Check if UI server is running."""
|
|
161
161
|
return is_ui_running(self._host, self._port)
|
|
162
162
|
|
|
163
|
-
def get_run_url(self, run_id: str) ->
|
|
163
|
+
def get_run_url(self, run_id: str) -> str | None:
|
|
164
164
|
"""Get URL to view a specific pipeline run.
|
|
165
165
|
|
|
166
166
|
Args:
|
|
@@ -174,7 +174,7 @@ class UIServerManager:
|
|
|
174
174
|
return f"{base_url}/runs/{run_id}"
|
|
175
175
|
return None
|
|
176
176
|
|
|
177
|
-
def get_pipeline_url(self, pipeline_name: str) ->
|
|
177
|
+
def get_pipeline_url(self, pipeline_name: str) -> str | None:
|
|
178
178
|
"""Get URL to view a specific pipeline.
|
|
179
179
|
|
|
180
180
|
Args:
|
flowyml/ui/utils.py
CHANGED
|
@@ -1,25 +1,128 @@
|
|
|
1
1
|
"""UI utility functions for checking UI server status and getting URLs."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import socket
|
|
4
5
|
import http.client
|
|
5
6
|
|
|
7
|
+
# Common ports to check for FlowyML unified server (backend serves frontend too)
|
|
8
|
+
# Priority order: 8080 (default), then alternates if port is busy
|
|
9
|
+
DEFAULT_SERVER_PORTS = [8080, 8081, 8082, 3000, 8000]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_available_port(start_port: int = 8081, max_attempts: int = 10) -> int:
|
|
13
|
+
"""Find an available port starting from the given port.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
start_port: Port to start checking from
|
|
17
|
+
max_attempts: Maximum number of ports to try
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
First available port found
|
|
21
|
+
"""
|
|
22
|
+
for offset in range(max_attempts):
|
|
23
|
+
port = start_port + offset
|
|
24
|
+
try:
|
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
26
|
+
s.bind(("localhost", port))
|
|
27
|
+
return port
|
|
28
|
+
except OSError:
|
|
29
|
+
continue
|
|
30
|
+
return start_port + max_attempts
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_port(host: str, port: int, path: str = "/", timeout: float = 1.0) -> bool:
|
|
34
|
+
"""Check if a port is responding to HTTP requests.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
host: Host to check
|
|
38
|
+
port: Port to check
|
|
39
|
+
path: Path to request
|
|
40
|
+
timeout: Connection timeout in seconds
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if port is responding, False otherwise
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
conn = http.client.HTTPConnection(host, port, timeout=timeout)
|
|
47
|
+
conn.request("GET", path)
|
|
48
|
+
response = conn.getresponse()
|
|
49
|
+
response.read() # Must read before closing
|
|
50
|
+
conn.close()
|
|
51
|
+
return response.status in (200, 301, 302, 404) # Accept common status codes
|
|
52
|
+
except Exception:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def discover_server(host: str = "localhost") -> tuple[str, int] | None:
|
|
57
|
+
"""Auto-discover the running FlowyML server.
|
|
58
|
+
|
|
59
|
+
The FlowyML backend serves both API and frontend on the same port.
|
|
60
|
+
This function finds where the unified server is running.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
host: Host to check
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (host, port) if found, None otherwise
|
|
67
|
+
"""
|
|
68
|
+
# First check environment variables
|
|
69
|
+
env_port = os.getenv("FLOWYML_UI_PORT") or os.getenv("FLOWYML_SERVER_PORT")
|
|
70
|
+
if env_port:
|
|
71
|
+
port = int(env_port)
|
|
72
|
+
if _check_port(host, port, "/api/health"):
|
|
73
|
+
return (host, port)
|
|
74
|
+
|
|
75
|
+
# Check environment URL
|
|
76
|
+
env_url = os.getenv("FLOWYML_REMOTE_SERVER_URL")
|
|
77
|
+
if env_url:
|
|
78
|
+
try:
|
|
79
|
+
from urllib.parse import urlparse
|
|
80
|
+
|
|
81
|
+
parsed = urlparse(env_url)
|
|
82
|
+
if parsed.port and _check_port(host, parsed.port, "/api/health"):
|
|
83
|
+
return (host, parsed.port)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# Check common server ports
|
|
88
|
+
for port in DEFAULT_SERVER_PORTS:
|
|
89
|
+
if _check_port(host, port, "/api/health"):
|
|
90
|
+
return (host, port)
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Keep these as aliases for backwards compatibility
|
|
96
|
+
def discover_ui_server(host: str = "localhost") -> tuple[str, int] | None:
|
|
97
|
+
"""Alias for discover_server (unified server serves both UI and API)."""
|
|
98
|
+
return discover_server(host)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def discover_api_server(host: str = "localhost") -> tuple[str, int] | None:
|
|
102
|
+
"""Alias for discover_server (unified server serves both UI and API)."""
|
|
103
|
+
return discover_server(host)
|
|
104
|
+
|
|
6
105
|
|
|
7
106
|
def get_ui_server_url() -> str:
|
|
8
|
-
"""Get the
|
|
107
|
+
"""Get the unified server URL, auto-discovering if possible.
|
|
108
|
+
|
|
109
|
+
The FlowyML backend serves both API and frontend on the same port.
|
|
9
110
|
|
|
10
111
|
Priority order:
|
|
11
112
|
1. FLOWYML_SERVER_URL environment variable (explicit override)
|
|
12
113
|
2. FLOWYML_REMOTE_UI_URL from config (for centralized deployments)
|
|
13
|
-
3.
|
|
14
|
-
4.
|
|
114
|
+
3. Auto-discover running server
|
|
115
|
+
4. FLOWYML_UI_HOST and FLOWYML_UI_PORT from config/env
|
|
116
|
+
5. Default: http://localhost:8080
|
|
15
117
|
|
|
16
118
|
Returns:
|
|
17
|
-
Base URL of the
|
|
119
|
+
Base URL of the server
|
|
18
120
|
"""
|
|
19
121
|
# Check for explicit server URL override
|
|
20
122
|
server_url = os.getenv("FLOWYML_SERVER_URL")
|
|
21
123
|
if server_url:
|
|
22
|
-
|
|
124
|
+
# Strip /api suffix if present since we want base URL
|
|
125
|
+
return server_url.rstrip("/").replace("/api", "")
|
|
23
126
|
|
|
24
127
|
# Check for remote UI URL (centralized deployment)
|
|
25
128
|
try:
|
|
@@ -28,29 +131,46 @@ def get_ui_server_url() -> str:
|
|
|
28
131
|
config = get_config()
|
|
29
132
|
if config.remote_ui_url:
|
|
30
133
|
return config.remote_ui_url.rstrip("/")
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
31
136
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
137
|
+
# Try auto-discovery
|
|
138
|
+
host = os.getenv("FLOWYML_UI_HOST", "localhost")
|
|
139
|
+
discovered = discover_server(host)
|
|
140
|
+
if discovered:
|
|
141
|
+
h, p = discovered
|
|
142
|
+
protocol = "https" if p == 443 else "http"
|
|
143
|
+
return f"{protocol}://{h}:{p}"
|
|
35
144
|
|
|
36
|
-
|
|
37
|
-
|
|
145
|
+
# Fallback to config or defaults
|
|
146
|
+
try:
|
|
147
|
+
from flowyml.utils.config import get_config
|
|
38
148
|
|
|
39
|
-
|
|
149
|
+
config = get_config()
|
|
150
|
+
host = os.getenv("FLOWYML_UI_HOST", config.ui_host)
|
|
151
|
+
port = int(os.getenv("FLOWYML_UI_PORT", str(config.ui_port)))
|
|
40
152
|
except Exception:
|
|
41
|
-
# Fallback to defaults
|
|
42
153
|
host = os.getenv("FLOWYML_UI_HOST", "localhost")
|
|
43
154
|
port = int(os.getenv("FLOWYML_UI_PORT", "8080"))
|
|
44
|
-
|
|
45
|
-
|
|
155
|
+
|
|
156
|
+
protocol = "https" if port == 443 else "http"
|
|
157
|
+
return f"{protocol}://{host}:{port}"
|
|
46
158
|
|
|
47
159
|
|
|
48
160
|
def get_ui_host_port() -> tuple[str, int]:
|
|
49
|
-
"""Get
|
|
161
|
+
"""Get server host and port, auto-discovering if possible.
|
|
50
162
|
|
|
51
163
|
Returns:
|
|
52
164
|
Tuple of (host, port)
|
|
53
165
|
"""
|
|
166
|
+
host = os.getenv("FLOWYML_UI_HOST", "localhost")
|
|
167
|
+
|
|
168
|
+
# Try auto-discovery first
|
|
169
|
+
discovered = discover_server(host)
|
|
170
|
+
if discovered:
|
|
171
|
+
return discovered
|
|
172
|
+
|
|
173
|
+
# Fallback to config or defaults
|
|
54
174
|
try:
|
|
55
175
|
from flowyml.utils.config import get_config
|
|
56
176
|
|
|
@@ -59,61 +179,59 @@ def get_ui_host_port() -> tuple[str, int]:
|
|
|
59
179
|
port = int(os.getenv("FLOWYML_UI_PORT", str(config.ui_port)))
|
|
60
180
|
return (host, port)
|
|
61
181
|
except Exception:
|
|
62
|
-
host = os.getenv("FLOWYML_UI_HOST", "localhost")
|
|
63
182
|
port = int(os.getenv("FLOWYML_UI_PORT", "8080"))
|
|
64
183
|
return (host, port)
|
|
65
184
|
|
|
66
185
|
|
|
67
|
-
def is_ui_running(host: str = "localhost", port: int =
|
|
186
|
+
def is_ui_running(host: str = "localhost", port: int | None = None) -> bool:
|
|
68
187
|
"""Check if the flowyml UI server is running.
|
|
69
188
|
|
|
70
189
|
Args:
|
|
71
190
|
host: Host to check (default: localhost)
|
|
72
|
-
port: Port to check (default:
|
|
191
|
+
port: Port to check (default: auto-discover)
|
|
73
192
|
|
|
74
193
|
Returns:
|
|
75
194
|
True if UI server is running and responding, False otherwise
|
|
76
195
|
"""
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
196
|
+
if port is None:
|
|
197
|
+
# Try auto-discovery
|
|
198
|
+
discovered = discover_ui_server(host)
|
|
199
|
+
return discovered is not None
|
|
81
200
|
|
|
82
|
-
|
|
83
|
-
# Note: must read data BEFORE closing connection
|
|
84
|
-
if response.status == 200:
|
|
85
|
-
data = response.read().decode("utf-8")
|
|
86
|
-
conn.close()
|
|
87
|
-
return "flowyml" in data.lower() or "ok" in data.lower()
|
|
88
|
-
conn.close()
|
|
89
|
-
return False
|
|
90
|
-
except Exception:
|
|
91
|
-
return False
|
|
201
|
+
return _check_port(host, port)
|
|
92
202
|
|
|
93
203
|
|
|
94
|
-
def get_ui_url(host: str = "localhost", port: int =
|
|
204
|
+
def get_ui_url(host: str = "localhost", port: int | None = None) -> str | None:
|
|
95
205
|
"""Get the URL of the running flowyml UI server.
|
|
96
206
|
|
|
97
207
|
Args:
|
|
98
208
|
host: Host of the UI server (default: localhost)
|
|
99
|
-
port: Port of the UI server (default:
|
|
209
|
+
port: Port of the UI server (default: auto-discover)
|
|
100
210
|
|
|
101
211
|
Returns:
|
|
102
212
|
URL string if server is running, None otherwise
|
|
103
213
|
"""
|
|
104
|
-
if
|
|
214
|
+
if port is None:
|
|
215
|
+
discovered = discover_ui_server(host)
|
|
216
|
+
if discovered:
|
|
217
|
+
h, p = discovered
|
|
218
|
+
protocol = "https" if p == 443 else "http"
|
|
219
|
+
return f"{protocol}://{h}:{p}"
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
if _check_port(host, port):
|
|
105
223
|
protocol = "https" if port == 443 else "http"
|
|
106
224
|
return f"{protocol}://{host}:{port}"
|
|
107
225
|
return None
|
|
108
226
|
|
|
109
227
|
|
|
110
|
-
def get_run_url(run_id: str, host: str = "localhost", port: int =
|
|
228
|
+
def get_run_url(run_id: str, host: str = "localhost", port: int | None = None) -> str | None:
|
|
111
229
|
"""Get the URL to view a specific pipeline run.
|
|
112
230
|
|
|
113
231
|
Args:
|
|
114
232
|
run_id: ID of the pipeline run
|
|
115
233
|
host: Host of the UI server (default: localhost)
|
|
116
|
-
port: Port of the UI server (default:
|
|
234
|
+
port: Port of the UI server (default: auto-discover)
|
|
117
235
|
|
|
118
236
|
Returns:
|
|
119
237
|
URL string to the run view if server is running, None otherwise
|
|
@@ -124,13 +242,13 @@ def get_run_url(run_id: str, host: str = "localhost", port: int = 8080) -> str |
|
|
|
124
242
|
return None
|
|
125
243
|
|
|
126
244
|
|
|
127
|
-
def get_pipeline_url(pipeline_name: str, host: str = "localhost", port: int =
|
|
245
|
+
def get_pipeline_url(pipeline_name: str, host: str = "localhost", port: int | None = None) -> str | None:
|
|
128
246
|
"""Get the URL to view a specific pipeline.
|
|
129
247
|
|
|
130
248
|
Args:
|
|
131
249
|
pipeline_name: Name of the pipeline
|
|
132
250
|
host: Host of the UI server (default: localhost)
|
|
133
|
-
port: Port of the UI server (default:
|
|
251
|
+
port: Port of the UI server (default: auto-discover)
|
|
134
252
|
|
|
135
253
|
Returns:
|
|
136
254
|
URL string to the pipeline view if server is running, None otherwise
|
flowyml/utils/config.py
CHANGED
|
@@ -25,6 +25,7 @@ class FlowymlConfig:
|
|
|
25
25
|
execution_mode: str = "local" # local or remote
|
|
26
26
|
remote_server_url: str = ""
|
|
27
27
|
remote_ui_url: str = ""
|
|
28
|
+
api_token: str | None = None
|
|
28
29
|
remote_services: list[dict[str, str]] = field(default_factory=list)
|
|
29
30
|
enable_caching: bool = True
|
|
30
31
|
enable_checkpointing: bool = True # Enable checkpointing by default
|
|
@@ -99,6 +100,7 @@ class FlowymlConfig:
|
|
|
99
100
|
"execution_mode": self.execution_mode,
|
|
100
101
|
"remote_server_url": self.remote_server_url,
|
|
101
102
|
"remote_ui_url": self.remote_ui_url,
|
|
103
|
+
"api_token": self.api_token,
|
|
102
104
|
"remote_services": self.remote_services,
|
|
103
105
|
"enable_caching": self.enable_caching,
|
|
104
106
|
"enable_checkpointing": self.enable_checkpointing,
|
|
@@ -184,6 +186,15 @@ def get_config() -> FlowymlConfig:
|
|
|
184
186
|
# Load from default location
|
|
185
187
|
_global_config = FlowymlConfig.load()
|
|
186
188
|
|
|
189
|
+
# Apply environment variable overrides
|
|
190
|
+
env_config = get_env_config()
|
|
191
|
+
for key, value in env_config.items():
|
|
192
|
+
if hasattr(_global_config, key):
|
|
193
|
+
setattr(_global_config, key, value)
|
|
194
|
+
|
|
195
|
+
# Re-normalize paths after env var overrides (env vars are strings, not Path objects)
|
|
196
|
+
_global_config.__post_init__()
|
|
197
|
+
|
|
187
198
|
# Create necessary directories
|
|
188
199
|
_global_config.create_directories()
|
|
189
200
|
|
|
@@ -202,9 +213,19 @@ def set_config(config: FlowymlConfig) -> None:
|
|
|
202
213
|
|
|
203
214
|
|
|
204
215
|
def reset_config() -> None:
|
|
205
|
-
"""Reset global configuration to defaults."""
|
|
216
|
+
"""Reset global configuration to defaults and apply environment variable overrides."""
|
|
206
217
|
global _global_config
|
|
207
218
|
_global_config = FlowymlConfig()
|
|
219
|
+
|
|
220
|
+
# Apply environment variable overrides
|
|
221
|
+
env_config = get_env_config()
|
|
222
|
+
for key, value in env_config.items():
|
|
223
|
+
if hasattr(_global_config, key):
|
|
224
|
+
setattr(_global_config, key, value)
|
|
225
|
+
|
|
226
|
+
# Re-normalize paths after env var overrides (env vars are strings, not Path objects)
|
|
227
|
+
_global_config.__post_init__()
|
|
228
|
+
|
|
208
229
|
_global_config.create_directories()
|
|
209
230
|
|
|
210
231
|
|
|
@@ -272,20 +293,21 @@ def get_env_config() -> dict[str, Any]:
|
|
|
272
293
|
|
|
273
294
|
# Map environment variables to config fields
|
|
274
295
|
env_mappings = {
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
296
|
+
"FLOWYML_HOME": "flowyml_home",
|
|
297
|
+
"FLOWYML_ARTIFACTS_DIR": "artifacts_dir",
|
|
298
|
+
"FLOWYML_METADATA_DB": "metadata_db",
|
|
299
|
+
"FLOWYML_CACHE_DIR": "cache_dir",
|
|
300
|
+
"FLOWYML_DEFAULT_STACK": "default_stack",
|
|
301
|
+
"FLOWYML_EXECUTION_MODE": "execution_mode",
|
|
302
|
+
"FLOWYML_REMOTE_SERVER_URL": "remote_server_url",
|
|
303
|
+
"FLOWYML_REMOTE_UI_URL": "remote_ui_url",
|
|
304
|
+
"FLOWYML_API_TOKEN": "api_token",
|
|
305
|
+
"FLOWYML_SERVER_URL": "remote_server_url", # Alias
|
|
306
|
+
"FLOWYML_ENABLE_CACHING": "enable_caching",
|
|
307
|
+
"FLOWYML_LOG_LEVEL": "log_level",
|
|
308
|
+
"FLOWYML_UI_HOST": "ui_host",
|
|
309
|
+
"FLOWYML_UI_PORT": "ui_port",
|
|
310
|
+
"FLOWYML_DEBUG": "debug_mode",
|
|
289
311
|
}
|
|
290
312
|
|
|
291
313
|
for env_var, config_key in env_mappings.items():
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Utilities for introspecting machine learning models."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def introspect_model(model: Any, framework: str) -> dict[str, Any]:
|
|
8
|
+
"""Introspect a model to extract input/output schema and metadata.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
model: The loaded model object
|
|
12
|
+
framework: Framework name (keras, tensorflow, sklearn, pytorch)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary containing schema information like:
|
|
16
|
+
- input_shape: List[int]
|
|
17
|
+
- output_shape: List[int]
|
|
18
|
+
- input_names: List[str]
|
|
19
|
+
- output_names: List[str]
|
|
20
|
+
- input_features: int
|
|
21
|
+
- total_params: int
|
|
22
|
+
"""
|
|
23
|
+
info = {
|
|
24
|
+
"framework": framework,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if framework in ["keras", "tensorflow"]:
|
|
28
|
+
_introspect_keras(model, info)
|
|
29
|
+
elif framework == "sklearn":
|
|
30
|
+
_introspect_sklearn(model, info)
|
|
31
|
+
elif framework == "pytorch":
|
|
32
|
+
_introspect_pytorch(model, info)
|
|
33
|
+
|
|
34
|
+
return info
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _introspect_keras(model: Any, info: dict[str, Any]) -> None:
|
|
38
|
+
"""Extract metadata from Keras/TensorFlow models."""
|
|
39
|
+
try:
|
|
40
|
+
# Input Shape
|
|
41
|
+
if hasattr(model, "input_shape"):
|
|
42
|
+
shape = model.input_shape
|
|
43
|
+
info["input_shape"] = [s if s else None for s in shape]
|
|
44
|
+
# Try to guess feature count from last dim
|
|
45
|
+
if isinstance(shape, (list, tuple)):
|
|
46
|
+
info["input_features"] = shape[-1] if len(shape) > 1 and shape[-1] else None
|
|
47
|
+
|
|
48
|
+
# Output Shape
|
|
49
|
+
if hasattr(model, "output_shape"):
|
|
50
|
+
shape = model.output_shape
|
|
51
|
+
info["output_shape"] = [s if s else None for s in shape]
|
|
52
|
+
|
|
53
|
+
# Input Names
|
|
54
|
+
if hasattr(model, "input_names") and model.input_names:
|
|
55
|
+
info["input_names"] = model.input_names
|
|
56
|
+
elif hasattr(model, "inputs"):
|
|
57
|
+
info["input_names"] = [inp.name.split(":")[0] for inp in model.inputs]
|
|
58
|
+
|
|
59
|
+
# Output Names
|
|
60
|
+
if hasattr(model, "output_names") and model.output_names:
|
|
61
|
+
info["output_names"] = model.output_names
|
|
62
|
+
elif hasattr(model, "outputs"):
|
|
63
|
+
info["output_names"] = [out.name.split(":")[0] for out in model.outputs]
|
|
64
|
+
|
|
65
|
+
# Layer info
|
|
66
|
+
if hasattr(model, "layers"):
|
|
67
|
+
info["layer_count"] = len(model.layers)
|
|
68
|
+
info["first_layer"] = model.layers[0].name if model.layers else None
|
|
69
|
+
info["last_layer"] = model.layers[-1].name if model.layers else None
|
|
70
|
+
|
|
71
|
+
# Params
|
|
72
|
+
with contextlib.suppress(Exception):
|
|
73
|
+
info["total_params"] = model.count_params()
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
info["introspection_error"] = str(e)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _introspect_sklearn(model: Any, info: dict[str, Any]) -> None:
|
|
80
|
+
"""Extract metadata from Scikit-Learn models."""
|
|
81
|
+
try:
|
|
82
|
+
if hasattr(model, "n_features_in_"):
|
|
83
|
+
info["input_features"] = model.n_features_in_
|
|
84
|
+
# Create synthetic input shape [None, n_features]
|
|
85
|
+
info["input_shape"] = [None, model.n_features_in_]
|
|
86
|
+
|
|
87
|
+
if hasattr(model, "feature_names_in_"):
|
|
88
|
+
info["input_names"] = list(model.feature_names_in_)
|
|
89
|
+
|
|
90
|
+
if hasattr(model, "classes_"):
|
|
91
|
+
info["classes"] = list(model.classes_)
|
|
92
|
+
info["output_shape"] = [None, 1] # Binary/Multi-class usually outputs 1 prediction or probas
|
|
93
|
+
|
|
94
|
+
if hasattr(model, "n_classes_"):
|
|
95
|
+
info["n_classes"] = model.n_classes_
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
info["introspection_error"] = str(e)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _introspect_pytorch(model: Any, info: dict[str, Any]) -> None:
|
|
102
|
+
"""Extract metadata from PyTorch models."""
|
|
103
|
+
try:
|
|
104
|
+
if hasattr(model, "parameters"):
|
|
105
|
+
info["total_params"] = sum(p.numel() for p in model.parameters())
|
|
106
|
+
|
|
107
|
+
# Try to infer input features from first layer if possible
|
|
108
|
+
# This is heuristic and might not work for all architectures
|
|
109
|
+
for _, module in getattr(model, "named_children", lambda: [])():
|
|
110
|
+
if hasattr(module, "in_features"):
|
|
111
|
+
info["input_features"] = module.in_features
|
|
112
|
+
info["input_shape"] = [None, module.in_features]
|
|
113
|
+
break
|
|
114
|
+
elif hasattr(module, "weight") and hasattr(module.weight, "shape") and len(module.weight.shape) >= 2:
|
|
115
|
+
# Conv2d weight: [out_channels, in_channels, kH, kW] -> unrelated to input *features* in flat sense usually
|
|
116
|
+
# Linear weight: [out_features, in_features]
|
|
117
|
+
if module.__class__.__name__ == "Linear":
|
|
118
|
+
info["input_features"] = module.weight.shape[1]
|
|
119
|
+
info["input_shape"] = [None, module.weight.shape[1]]
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
info["introspection_error"] = str(e)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Any
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
|
|
6
|
+
tracer = trace.get_tracer("flowyml")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def trace_execution(operation_name: str | None = None) -> Callable:
|
|
10
|
+
"""Decorator to trace function execution with OpenTelemetry."""
|
|
11
|
+
|
|
12
|
+
def decorator(func: Callable) -> Callable:
|
|
13
|
+
@wraps(func)
|
|
14
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
15
|
+
span_name = operation_name or func.__name__
|
|
16
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
17
|
+
# Add basic attributes
|
|
18
|
+
span.set_attribute("function.name", func.__name__)
|
|
19
|
+
span.set_attribute("function.module", func.__module__)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
return func(*args, **kwargs)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
span.record_exception(e)
|
|
25
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR))
|
|
26
|
+
raise e
|
|
27
|
+
|
|
28
|
+
return wrapper
|
|
29
|
+
|
|
30
|
+
return decorator
|