flowyml 1.7.2__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.
Files changed (126) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/metrics.py +5 -0
  3. flowyml/cli/main.py +709 -0
  4. flowyml/cli/stack_cli.py +138 -25
  5. flowyml/core/__init__.py +17 -0
  6. flowyml/core/executor.py +161 -26
  7. flowyml/core/image_builder.py +129 -0
  8. flowyml/core/log_streamer.py +227 -0
  9. flowyml/core/orchestrator.py +22 -2
  10. flowyml/core/pipeline.py +34 -10
  11. flowyml/core/routing.py +558 -0
  12. flowyml/core/step.py +9 -1
  13. flowyml/core/step_grouping.py +49 -35
  14. flowyml/core/types.py +407 -0
  15. flowyml/monitoring/alerts.py +10 -0
  16. flowyml/monitoring/notifications.py +104 -25
  17. flowyml/monitoring/slack_blocks.py +323 -0
  18. flowyml/plugins/__init__.py +251 -0
  19. flowyml/plugins/alerters/__init__.py +1 -0
  20. flowyml/plugins/alerters/slack.py +168 -0
  21. flowyml/plugins/base.py +752 -0
  22. flowyml/plugins/config.py +478 -0
  23. flowyml/plugins/deployers/__init__.py +22 -0
  24. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  25. flowyml/plugins/deployers/sagemaker.py +306 -0
  26. flowyml/plugins/deployers/vertex.py +290 -0
  27. flowyml/plugins/integration.py +369 -0
  28. flowyml/plugins/manager.py +510 -0
  29. flowyml/plugins/model_registries/__init__.py +22 -0
  30. flowyml/plugins/model_registries/mlflow.py +159 -0
  31. flowyml/plugins/model_registries/sagemaker.py +489 -0
  32. flowyml/plugins/model_registries/vertex.py +386 -0
  33. flowyml/plugins/orchestrators/__init__.py +13 -0
  34. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  35. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  36. flowyml/plugins/registries/__init__.py +13 -0
  37. flowyml/plugins/registries/ecr.py +321 -0
  38. flowyml/plugins/registries/gcr.py +313 -0
  39. flowyml/plugins/registry.py +454 -0
  40. flowyml/plugins/stack.py +494 -0
  41. flowyml/plugins/stack_config.py +537 -0
  42. flowyml/plugins/stores/__init__.py +13 -0
  43. flowyml/plugins/stores/gcs.py +460 -0
  44. flowyml/plugins/stores/s3.py +453 -0
  45. flowyml/plugins/trackers/__init__.py +11 -0
  46. flowyml/plugins/trackers/mlflow.py +316 -0
  47. flowyml/plugins/validators/__init__.py +3 -0
  48. flowyml/plugins/validators/deepchecks.py +119 -0
  49. flowyml/registry/__init__.py +2 -1
  50. flowyml/registry/model_environment.py +109 -0
  51. flowyml/registry/model_registry.py +241 -96
  52. flowyml/serving/__init__.py +17 -0
  53. flowyml/serving/model_server.py +628 -0
  54. flowyml/stacks/__init__.py +60 -0
  55. flowyml/stacks/aws.py +93 -0
  56. flowyml/stacks/base.py +62 -0
  57. flowyml/stacks/components.py +12 -0
  58. flowyml/stacks/gcp.py +44 -9
  59. flowyml/stacks/plugins.py +115 -0
  60. flowyml/stacks/registry.py +2 -1
  61. flowyml/storage/sql.py +401 -12
  62. flowyml/tracking/experiment.py +8 -5
  63. flowyml/ui/backend/Dockerfile +87 -16
  64. flowyml/ui/backend/auth.py +12 -2
  65. flowyml/ui/backend/main.py +149 -5
  66. flowyml/ui/backend/routers/ai_context.py +226 -0
  67. flowyml/ui/backend/routers/assets.py +23 -4
  68. flowyml/ui/backend/routers/auth.py +96 -0
  69. flowyml/ui/backend/routers/deployments.py +660 -0
  70. flowyml/ui/backend/routers/model_explorer.py +597 -0
  71. flowyml/ui/backend/routers/plugins.py +103 -51
  72. flowyml/ui/backend/routers/projects.py +91 -8
  73. flowyml/ui/backend/routers/runs.py +20 -1
  74. flowyml/ui/backend/routers/schedules.py +22 -17
  75. flowyml/ui/backend/routers/templates.py +319 -0
  76. flowyml/ui/backend/routers/websocket.py +2 -2
  77. flowyml/ui/frontend/Dockerfile +55 -6
  78. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  79. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  80. flowyml/ui/frontend/dist/index.html +2 -2
  81. flowyml/ui/frontend/dist/logo.png +0 -0
  82. flowyml/ui/frontend/nginx.conf +65 -4
  83. flowyml/ui/frontend/package-lock.json +1404 -74
  84. flowyml/ui/frontend/package.json +3 -0
  85. flowyml/ui/frontend/public/logo.png +0 -0
  86. flowyml/ui/frontend/src/App.jsx +10 -7
  87. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  88. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  89. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  90. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  91. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  92. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  93. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
  94. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  95. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  96. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
  97. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  98. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  99. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  100. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  101. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  102. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  103. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  104. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  105. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  106. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  107. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  108. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  109. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  110. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  111. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  112. flowyml/ui/frontend/src/router/index.jsx +47 -20
  113. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  114. flowyml/ui/server_manager.py +5 -5
  115. flowyml/ui/utils.py +157 -39
  116. flowyml/utils/config.py +37 -15
  117. flowyml/utils/model_introspection.py +123 -0
  118. flowyml/utils/observability.py +30 -0
  119. flowyml-1.8.0.dist-info/METADATA +174 -0
  120. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
  121. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  122. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
  123. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
  124. flowyml-1.7.2.dist-info/METADATA +0 -477
  125. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  126. {flowyml-1.7.2.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
- path: '/',
25
- element: <MainLayout />,
36
+ element: <AppLayout />, // Wrap everything in AuthProvider
26
37
  children: [
27
- { index: true, element: <Dashboard /> },
28
- { path: 'pipelines', element: <Pipelines /> },
29
- { path: 'runs', element: <Runs /> },
30
- { path: 'compare', element: <RunComparisonPage /> },
31
- { path: 'runs/:runId', element: <RunDetails /> },
32
- { path: 'assets', element: <Assets /> },
33
- { path: 'experiments', element: <Experiments /> },
34
- { path: 'experiments/compare', element: <ExperimentComparisonPage /> },
35
- { path: 'experiments/:experimentId', element: <ExperimentDetails /> },
36
- { path: 'traces', element: <Traces /> },
37
- { path: 'projects', element: <Projects /> },
38
- { path: 'projects/:projectId', element: <ProjectDetails /> },
39
- { path: 'schedules', element: <Schedules /> },
40
- { path: 'observability', element: <Observability /> },
41
- { path: 'leaderboard', element: <Leaderboard /> },
42
- { path: 'plugins', element: <Plugins /> },
43
- { path: 'settings', element: <Settings /> },
44
- { path: 'tokens', element: <TokenManagement /> },
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 importZenMLStack(stackName) {
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' },
@@ -15,8 +15,8 @@ class UIServerManager:
15
15
  _lock = threading.Lock()
16
16
 
17
17
  def __init__(self):
18
- self._server_thread: Optional[threading.Thread] = None
19
- self._server_process: Optional[subprocess.Popen] = None
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) -> Optional[str]:
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) -> Optional[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) -> Optional[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 UI server URL from configuration or environment variables.
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. FLOWYML_UI_HOST and FLOWYML_UI_PORT from config/env
14
- 4. Default: http://localhost:8080
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 UI server (e.g., "http://localhost:8080" or "https://flowyml.example.com")
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
- return server_url.rstrip("/")
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
- # Use config values for host/port
33
- host = os.getenv("FLOWYML_UI_HOST", config.ui_host)
34
- port = int(os.getenv("FLOWYML_UI_PORT", str(config.ui_port)))
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
- # Determine protocol based on port (443 = https, else http)
37
- protocol = "https" if port == 443 else "http"
145
+ # Fallback to config or defaults
146
+ try:
147
+ from flowyml.utils.config import get_config
38
148
 
39
- return f"{protocol}://{host}:{port}"
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
- protocol = "https" if port == 443 else "http"
45
- return f"{protocol}://{host}:{port}"
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 UI host and port from configuration.
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 = 8080) -> bool:
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: 8080)
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
- try:
78
- conn = http.client.HTTPConnection(host, port, timeout=2)
79
- conn.request("GET", "/api/health")
80
- response = conn.getresponse()
196
+ if port is None:
197
+ # Try auto-discovery
198
+ discovered = discover_ui_server(host)
199
+ return discovered is not None
81
200
 
82
- # Check if response is successful and from flowyml
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 = 8080) -> str | None:
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: 8080)
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 is_ui_running(host, port):
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 = 8080) -> str | None:
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: 8080)
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 = 8080) -> str | None:
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: 8080)
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
- "flowyml_HOME": "flowyml_home",
276
- "flowyml_ARTIFACTS_DIR": "artifacts_dir",
277
- "flowyml_METADATA_DB": "metadata_db",
278
- "flowyml_CACHE_DIR": "cache_dir",
279
- "flowyml_DEFAULT_STACK": "default_stack",
280
- "flowyml_EXECUTION_MODE": "execution_mode",
281
- "flowyml_REMOTE_SERVER_URL": "remote_server_url",
282
- "flowyml_REMOTE_UI_URL": "remote_ui_url",
283
- "flowyml_SERVER_URL": "remote_ui_url", # Alias for FLOWYML_SERVER_URL -> remote_ui_url
284
- "flowyml_ENABLE_CACHING": "enable_caching",
285
- "flowyml_LOG_LEVEL": "log_level",
286
- "flowyml_UI_HOST": "ui_host",
287
- "flowyml_UI_PORT": "ui_port",
288
- "flowyml_DEBUG": "debug_mode",
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