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
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""FlowyML Plugin Manager - Installation, Loading, and Discovery.
|
|
2
|
+
|
|
3
|
+
This module provides the main interface for managing plugins:
|
|
4
|
+
installing dependencies, loading plugin classes, and discovering
|
|
5
|
+
available plugins.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from flowyml.plugins.manager import PluginManager
|
|
9
|
+
|
|
10
|
+
manager = PluginManager()
|
|
11
|
+
|
|
12
|
+
# Install a plugin
|
|
13
|
+
manager.install("mlflow")
|
|
14
|
+
|
|
15
|
+
# Load and use a plugin
|
|
16
|
+
MLflowTracker = manager.load("mlflow")
|
|
17
|
+
tracker = MLflowTracker(tracking_uri="http://localhost:5000")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import importlib
|
|
23
|
+
import importlib.util
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
from flowyml.plugins.base import BasePlugin, PluginType
|
|
27
|
+
from flowyml.plugins.registry import (
|
|
28
|
+
PLUGIN_CATALOG,
|
|
29
|
+
PluginInfo,
|
|
30
|
+
PluginStatus,
|
|
31
|
+
get_plugin_info,
|
|
32
|
+
list_plugin_names,
|
|
33
|
+
register_plugin,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PluginManager:
|
|
40
|
+
"""Manages FlowyML plugin lifecycle.
|
|
41
|
+
|
|
42
|
+
The PluginManager handles:
|
|
43
|
+
- Installing plugin dependencies
|
|
44
|
+
- Loading plugin classes
|
|
45
|
+
- Discovering available and installed plugins
|
|
46
|
+
- Managing plugin instances
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
manager = PluginManager()
|
|
50
|
+
|
|
51
|
+
# Check what's available
|
|
52
|
+
print(manager.list_available())
|
|
53
|
+
|
|
54
|
+
# Install a plugin (installs underlying packages)
|
|
55
|
+
manager.install("mlflow")
|
|
56
|
+
|
|
57
|
+
# Load the plugin class
|
|
58
|
+
MLflowTracker = manager.load("mlflow")
|
|
59
|
+
|
|
60
|
+
# Create an instance
|
|
61
|
+
tracker = MLflowTracker(tracking_uri="http://localhost:5000")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Cache of loaded plugin classes
|
|
65
|
+
_loaded_plugins: dict[str, type[BasePlugin]] = {}
|
|
66
|
+
|
|
67
|
+
# Cache of plugin instances
|
|
68
|
+
_instances: dict[str, BasePlugin] = {}
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
"""Initialize the plugin manager."""
|
|
72
|
+
self._check_installed_status()
|
|
73
|
+
|
|
74
|
+
def _check_installed_status(self) -> None:
|
|
75
|
+
"""Check which plugins have their packages installed."""
|
|
76
|
+
for _name, info in PLUGIN_CATALOG.items():
|
|
77
|
+
if self._are_packages_installed(info.packages):
|
|
78
|
+
info.status = PluginStatus.INSTALLED
|
|
79
|
+
|
|
80
|
+
def _are_packages_installed(self, packages: list[str]) -> bool:
|
|
81
|
+
"""Check if all required packages are installed.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
packages: List of package requirements (e.g., ["mlflow>=2.0"])
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if all packages are installed.
|
|
88
|
+
"""
|
|
89
|
+
for pkg_spec in packages:
|
|
90
|
+
# Extract package name from spec (e.g., "mlflow>=2.0" -> "mlflow")
|
|
91
|
+
pkg_name = pkg_spec.split(">=")[0].split("==")[0].split("<")[0].strip()
|
|
92
|
+
# Handle packages with dashes vs underscores
|
|
93
|
+
pkg_name = pkg_name.replace("-", "_")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if importlib.util.find_spec(pkg_name) is None:
|
|
97
|
+
# Try with dashes
|
|
98
|
+
pkg_name_dash = pkg_name.replace("_", "-")
|
|
99
|
+
if importlib.util.find_spec(pkg_name_dash) is None:
|
|
100
|
+
return False
|
|
101
|
+
except (ValueError, AttributeError):
|
|
102
|
+
# This can happen if a module is partially loaded or mocked without __spec__
|
|
103
|
+
return False
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# =========================================================================
|
|
107
|
+
# DISCOVERY METHODS
|
|
108
|
+
# =========================================================================
|
|
109
|
+
|
|
110
|
+
def list_available(self, plugin_type: PluginType = None) -> list[str]:
|
|
111
|
+
"""List all available plugins.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
plugin_type: Optional filter by plugin type.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of plugin names.
|
|
118
|
+
"""
|
|
119
|
+
return list_plugin_names(plugin_type)
|
|
120
|
+
|
|
121
|
+
def list_installed(self, plugin_type: PluginType = None) -> list[str]:
|
|
122
|
+
"""List plugins that have their packages installed.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
plugin_type: Optional filter by plugin type.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of installed plugin names.
|
|
129
|
+
"""
|
|
130
|
+
installed = []
|
|
131
|
+
for name, info in PLUGIN_CATALOG.items():
|
|
132
|
+
if plugin_type and info.plugin_type != plugin_type:
|
|
133
|
+
continue
|
|
134
|
+
if self._are_packages_installed(info.packages):
|
|
135
|
+
installed.append(name)
|
|
136
|
+
return installed
|
|
137
|
+
|
|
138
|
+
def get_info(self, name: str) -> PluginInfo | None:
|
|
139
|
+
"""Get information about a plugin.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
name: Plugin name.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
PluginInfo if found, None otherwise.
|
|
146
|
+
"""
|
|
147
|
+
return get_plugin_info(name)
|
|
148
|
+
|
|
149
|
+
def is_installed(self, name: str) -> bool:
|
|
150
|
+
"""Check if a plugin's packages are installed.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Plugin name.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if installed.
|
|
157
|
+
"""
|
|
158
|
+
info = get_plugin_info(name)
|
|
159
|
+
if not info:
|
|
160
|
+
return False
|
|
161
|
+
return self._are_packages_installed(info.packages)
|
|
162
|
+
|
|
163
|
+
# =========================================================================
|
|
164
|
+
# INSTALLATION METHODS
|
|
165
|
+
# =========================================================================
|
|
166
|
+
|
|
167
|
+
def install(self, name: str, upgrade: bool = False) -> bool:
|
|
168
|
+
"""Install a plugin and its dependencies.
|
|
169
|
+
|
|
170
|
+
This installs the underlying packages directly (e.g., mlflow, boto3)
|
|
171
|
+
without requiring any external framework.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: Plugin name to install.
|
|
175
|
+
upgrade: If True, upgrade packages to latest versions.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if installation was successful.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
ValueError: If plugin is not found in catalog.
|
|
182
|
+
"""
|
|
183
|
+
info = get_plugin_info(name)
|
|
184
|
+
if not info:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Plugin '{name}' not found. " f"Available plugins: {', '.join(list_plugin_names())}",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
logger.info(f"Installing plugin '{name}' with packages: {info.packages}")
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Build pip install command
|
|
193
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
194
|
+
if upgrade:
|
|
195
|
+
cmd.append("--upgrade")
|
|
196
|
+
cmd.extend(info.packages)
|
|
197
|
+
|
|
198
|
+
# Run installation
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
cmd,
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if result.returncode == 0:
|
|
206
|
+
info.status = PluginStatus.INSTALLED
|
|
207
|
+
logger.info(f"Successfully installed plugin '{name}'")
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
logger.error(f"Failed to install '{name}': {result.stderr}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Error installing plugin '{name}': {e}")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def install_all(self, plugin_type: PluginType = None) -> dict[str, bool]:
|
|
218
|
+
"""Install all plugins (or all of a specific type).
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
plugin_type: Optional filter by plugin type.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Dictionary mapping plugin names to success status.
|
|
225
|
+
"""
|
|
226
|
+
results = {}
|
|
227
|
+
for name in self.list_available(plugin_type):
|
|
228
|
+
results[name] = self.install(name)
|
|
229
|
+
return results
|
|
230
|
+
|
|
231
|
+
def uninstall(self, name: str) -> bool:
|
|
232
|
+
"""Uninstall a plugin's packages.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Plugin name to uninstall.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if uninstallation was successful.
|
|
239
|
+
"""
|
|
240
|
+
info = get_plugin_info(name)
|
|
241
|
+
if not info:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Extract package names
|
|
246
|
+
pkg_names = []
|
|
247
|
+
for pkg_spec in info.packages:
|
|
248
|
+
pkg_name = pkg_spec.split(">=")[0].split("==")[0].split("<")[0].strip()
|
|
249
|
+
pkg_names.append(pkg_name)
|
|
250
|
+
|
|
251
|
+
cmd = [sys.executable, "-m", "pip", "uninstall", "-y"] + pkg_names
|
|
252
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
253
|
+
|
|
254
|
+
if result.returncode == 0:
|
|
255
|
+
info.status = PluginStatus.AVAILABLE
|
|
256
|
+
# Remove from loaded cache
|
|
257
|
+
self._loaded_plugins.pop(name, None)
|
|
258
|
+
self._instances.pop(name, None)
|
|
259
|
+
logger.info(f"Uninstalled plugin '{name}'")
|
|
260
|
+
return True
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Error uninstalling plugin '{name}': {e}")
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
# =========================================================================
|
|
268
|
+
# LOADING METHODS
|
|
269
|
+
# =========================================================================
|
|
270
|
+
|
|
271
|
+
def load(self, name: str) -> type[BasePlugin]:
|
|
272
|
+
"""Load a plugin class.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
name: Plugin name to load.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The plugin class (not instantiated).
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
ValueError: If plugin not found.
|
|
282
|
+
ImportError: If plugin packages not installed.
|
|
283
|
+
"""
|
|
284
|
+
# Check cache first
|
|
285
|
+
if name in self._loaded_plugins:
|
|
286
|
+
return self._loaded_plugins[name]
|
|
287
|
+
|
|
288
|
+
info = get_plugin_info(name)
|
|
289
|
+
if not info:
|
|
290
|
+
raise ValueError(f"Plugin '{name}' not found in catalog")
|
|
291
|
+
|
|
292
|
+
if not self._are_packages_installed(info.packages):
|
|
293
|
+
raise ImportError(
|
|
294
|
+
f"Plugin '{name}' packages not installed. " f"Run: flowyml plugin install {name}",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Load the plugin class
|
|
298
|
+
try:
|
|
299
|
+
module_path, class_name = info.wrapper_path.rsplit(":", 1)
|
|
300
|
+
module = importlib.import_module(module_path)
|
|
301
|
+
plugin_class = getattr(module, class_name)
|
|
302
|
+
|
|
303
|
+
# Cache and return
|
|
304
|
+
self._loaded_plugins[name] = plugin_class
|
|
305
|
+
info.status = PluginStatus.LOADED
|
|
306
|
+
return plugin_class
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.error(f"Error loading plugin '{name}': {e}")
|
|
310
|
+
raise ImportError(f"Could not load plugin '{name}': {e}")
|
|
311
|
+
|
|
312
|
+
def get_instance(
|
|
313
|
+
self,
|
|
314
|
+
name: str,
|
|
315
|
+
instance_name: str = None,
|
|
316
|
+
**config,
|
|
317
|
+
) -> BasePlugin:
|
|
318
|
+
"""Get or create a plugin instance.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
name: Plugin name.
|
|
322
|
+
instance_name: Optional name for this instance (for caching).
|
|
323
|
+
**config: Configuration for the plugin.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Plugin instance.
|
|
327
|
+
"""
|
|
328
|
+
cache_key = f"{name}:{instance_name}" if instance_name else name
|
|
329
|
+
|
|
330
|
+
if cache_key in self._instances:
|
|
331
|
+
return self._instances[cache_key]
|
|
332
|
+
|
|
333
|
+
plugin_class = self.load(name)
|
|
334
|
+
instance = plugin_class(name=instance_name, **config)
|
|
335
|
+
|
|
336
|
+
if instance_name:
|
|
337
|
+
self._instances[cache_key] = instance
|
|
338
|
+
|
|
339
|
+
return instance
|
|
340
|
+
|
|
341
|
+
# =========================================================================
|
|
342
|
+
# COMMUNITY PLUGIN SUPPORT
|
|
343
|
+
# =========================================================================
|
|
344
|
+
|
|
345
|
+
def install_from_git(self, git_url: str, name: str = None) -> bool:
|
|
346
|
+
"""Install a community plugin from a git repository.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
git_url: Git URL (e.g., https://github.com/user/flowyml-plugin.git)
|
|
350
|
+
name: Optional plugin name override.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if installation was successful.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
cmd = [sys.executable, "-m", "pip", "install", f"git+{git_url}"]
|
|
357
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
358
|
+
|
|
359
|
+
if result.returncode == 0:
|
|
360
|
+
logger.info(f"Installed community plugin from {git_url}")
|
|
361
|
+
# Try to discover and register the plugin
|
|
362
|
+
self._discover_entrypoint_plugins()
|
|
363
|
+
return True
|
|
364
|
+
else:
|
|
365
|
+
logger.error(f"Failed to install from git: {result.stderr}")
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.error(f"Error installing from git: {e}")
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
def install_from_path(self, path: str) -> bool:
|
|
373
|
+
"""Install a plugin from a local path.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
path: Path to the plugin package.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
True if installation was successful.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
cmd = [sys.executable, "-m", "pip", "install", "-e", path]
|
|
383
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
384
|
+
|
|
385
|
+
if result.returncode == 0:
|
|
386
|
+
logger.info(f"Installed plugin from {path}")
|
|
387
|
+
self._discover_entrypoint_plugins()
|
|
388
|
+
return True
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Error installing from path: {e}")
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
def _discover_entrypoint_plugins(self) -> None:
|
|
396
|
+
"""Discover plugins registered via entry points.
|
|
397
|
+
|
|
398
|
+
Community plugins can register themselves by adding an entry point
|
|
399
|
+
in their setup.py or pyproject.toml:
|
|
400
|
+
|
|
401
|
+
[project.entry-points."flowyml.plugins"]
|
|
402
|
+
my_plugin = "my_package.plugins:MyPlugin"
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
# Python 3.10+
|
|
406
|
+
from importlib.metadata import entry_points
|
|
407
|
+
|
|
408
|
+
eps = entry_points()
|
|
409
|
+
if hasattr(eps, "select"):
|
|
410
|
+
# Python 3.10+
|
|
411
|
+
flowyml_plugins = eps.select(group="flowyml.plugins")
|
|
412
|
+
else:
|
|
413
|
+
# Python 3.9
|
|
414
|
+
flowyml_plugins = eps.get("flowyml.plugins", [])
|
|
415
|
+
|
|
416
|
+
for ep in flowyml_plugins:
|
|
417
|
+
try:
|
|
418
|
+
plugin_class = ep.load()
|
|
419
|
+
metadata = getattr(plugin_class, "METADATA", None)
|
|
420
|
+
|
|
421
|
+
if metadata:
|
|
422
|
+
info = PluginInfo(
|
|
423
|
+
name=metadata.name,
|
|
424
|
+
description=metadata.description,
|
|
425
|
+
plugin_type=metadata.plugin_type,
|
|
426
|
+
packages=metadata.packages,
|
|
427
|
+
wrapper_path=f"{plugin_class.__module__}:{plugin_class.__name__}",
|
|
428
|
+
version=metadata.version,
|
|
429
|
+
author=metadata.author,
|
|
430
|
+
status=PluginStatus.INSTALLED,
|
|
431
|
+
)
|
|
432
|
+
register_plugin(info)
|
|
433
|
+
logger.info(f"Discovered community plugin: {metadata.name}")
|
|
434
|
+
else:
|
|
435
|
+
logger.warning(f"Plugin {ep.name} missing METADATA")
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.debug(f"Could not load entry point {ep.name}: {e}")
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.debug(f"Could not discover entry points: {e}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ============================================================================
|
|
445
|
+
# MODULE-LEVEL CONVENIENCE FUNCTIONS
|
|
446
|
+
# ============================================================================
|
|
447
|
+
|
|
448
|
+
# Global manager instance
|
|
449
|
+
_manager: PluginManager | None = None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def get_manager() -> PluginManager:
|
|
453
|
+
"""Get the global plugin manager instance."""
|
|
454
|
+
global _manager
|
|
455
|
+
if _manager is None:
|
|
456
|
+
_manager = PluginManager()
|
|
457
|
+
return _manager
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def install(name: str, upgrade: bool = False) -> bool:
|
|
461
|
+
"""Install a plugin.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
name: Plugin name.
|
|
465
|
+
upgrade: If True, upgrade packages.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
True if successful.
|
|
469
|
+
"""
|
|
470
|
+
return get_manager().install(name, upgrade)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def load(name: str) -> type[BasePlugin]:
|
|
474
|
+
"""Load a plugin class.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
name: Plugin name.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
The plugin class.
|
|
481
|
+
"""
|
|
482
|
+
return get_manager().load(name)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def get_plugin(name: str, **config) -> BasePlugin:
|
|
486
|
+
"""Get a plugin instance.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
name: Plugin name.
|
|
490
|
+
**config: Plugin configuration.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Plugin instance.
|
|
494
|
+
"""
|
|
495
|
+
return get_manager().get_instance(name, **config)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def list_available(plugin_type: PluginType = None) -> list[str]:
|
|
499
|
+
"""List available plugins."""
|
|
500
|
+
return get_manager().list_available(plugin_type)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def list_installed(plugin_type: PluginType = None) -> list[str]:
|
|
504
|
+
"""List installed plugins."""
|
|
505
|
+
return get_manager().list_installed(plugin_type)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def is_installed(name: str) -> bool:
|
|
509
|
+
"""Check if a plugin is installed."""
|
|
510
|
+
return get_manager().is_installed(name)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""FlowyML Model Registry Plugins - Native implementations for ML model registries.
|
|
2
|
+
|
|
3
|
+
This module provides model registry plugin implementations for:
|
|
4
|
+
- Vertex AI Model Registry (GCP)
|
|
5
|
+
- SageMaker Model Registry (AWS)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from flowyml.plugins.model_registries import VertexModelRegistry
|
|
9
|
+
|
|
10
|
+
# Or via config
|
|
11
|
+
# model_registry:
|
|
12
|
+
# type: vertex_model_registry
|
|
13
|
+
# project: my-gcp-project
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from flowyml.plugins.model_registries.vertex import VertexModelRegistry
|
|
17
|
+
from flowyml.plugins.model_registries.sagemaker import SageMakerModelRegistry
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"VertexModelRegistry",
|
|
21
|
+
"SageMakerModelRegistry",
|
|
22
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""MLflow Model Registry - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This plugin provides explicit Model Registry capabilities using MLflow,
|
|
4
|
+
allowing distinct management from Experiment Tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
from flowyml.utils.observability import trace_execution
|
|
10
|
+
|
|
11
|
+
from flowyml.plugins.base import ModelRegistryPlugin, PluginMetadata, PluginType
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# import removed from here
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MLflowModelRegistry(ModelRegistryPlugin):
|
|
20
|
+
"""Native MLflow Model Registry for FlowyML.
|
|
21
|
+
|
|
22
|
+
Manage model lifecycles (registration, versioning, stage transition)
|
|
23
|
+
directly through MLflow.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
registry_uri: URI for the registry (e.g., sqlite:///, postgresql://).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
metadata = PluginMetadata(
|
|
30
|
+
name="mlflow_registry",
|
|
31
|
+
version="1.0.0",
|
|
32
|
+
description="MLflow Model Registry",
|
|
33
|
+
author="FlowyML Team",
|
|
34
|
+
plugin_type=PluginType.MODEL_REGISTRY,
|
|
35
|
+
tags=["model-registry", "mlflow", "versioning"],
|
|
36
|
+
packages=["mlflow>=2.0"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __init__(self, registry_uri: str = None, **kwargs):
|
|
40
|
+
super().__init__(**kwargs)
|
|
41
|
+
self.registry_uri = registry_uri
|
|
42
|
+
self._client = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def plugin_type(self) -> PluginType:
|
|
46
|
+
return PluginType.MODEL_REGISTRY
|
|
47
|
+
|
|
48
|
+
def initialize(self) -> None:
|
|
49
|
+
"""Initialize MLflow Client."""
|
|
50
|
+
try:
|
|
51
|
+
import mlflow
|
|
52
|
+
from mlflow.tracking import MlflowClient
|
|
53
|
+
|
|
54
|
+
if self.registry_uri:
|
|
55
|
+
mlflow.set_registry_uri(self.registry_uri)
|
|
56
|
+
|
|
57
|
+
self._client = MlflowClient(registry_uri=self.registry_uri)
|
|
58
|
+
logger.info(f"MLflow Model Registry initialized (URI: {self.registry_uri or 'default'})")
|
|
59
|
+
|
|
60
|
+
except ImportError:
|
|
61
|
+
raise ImportError(
|
|
62
|
+
"mlflow is required. Install with: pip install mlflow",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@trace_execution(operation_name="mlflow_register_model")
|
|
66
|
+
def register_model(
|
|
67
|
+
self,
|
|
68
|
+
name: str,
|
|
69
|
+
model_uri: str,
|
|
70
|
+
version: str = None,
|
|
71
|
+
metadata: dict = None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Register a model artifact as a new model version.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: Name of the registered model.
|
|
77
|
+
model_uri: Source URI of the model artifact (e.g., runs:/.../model).
|
|
78
|
+
version: Ignored by MLflow (auto-incremented).
|
|
79
|
+
metadata: Tags/Description to set.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The new model version number.
|
|
83
|
+
"""
|
|
84
|
+
self.initialize()
|
|
85
|
+
|
|
86
|
+
# 1. Create registered model if it doesn't exist
|
|
87
|
+
try:
|
|
88
|
+
self._client.create_registered_model(name)
|
|
89
|
+
logger.info(f"Created new registered model: {name}")
|
|
90
|
+
except Exception:
|
|
91
|
+
# Assume exists
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# 2. Create version
|
|
95
|
+
mv = self._client.create_model_version(
|
|
96
|
+
name=name,
|
|
97
|
+
source=model_uri,
|
|
98
|
+
run_id=None, # Derive from source if possible
|
|
99
|
+
tags=metadata,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
logger.info(f"Registered model '{name}' version {mv.version}")
|
|
103
|
+
return mv.version
|
|
104
|
+
|
|
105
|
+
@trace_execution(operation_name="mlflow_get_model")
|
|
106
|
+
def get_model(self, name: str, version: str = None) -> Any:
|
|
107
|
+
"""Get model version details (metadata).
|
|
108
|
+
To load the actual model object, use ExperimentTracker.load_model or standard mlflow.load_model.
|
|
109
|
+
"""
|
|
110
|
+
self.initialize()
|
|
111
|
+
if version:
|
|
112
|
+
return self._client.get_model_version(name, version)
|
|
113
|
+
else:
|
|
114
|
+
# Get latest
|
|
115
|
+
# MLflow specific: get latest versions for all stages
|
|
116
|
+
return self._client.get_latest_versions(name, stages=None)
|
|
117
|
+
|
|
118
|
+
@trace_execution(operation_name="mlflow_list_models")
|
|
119
|
+
def list_models(self, name: str = None) -> list[dict]:
|
|
120
|
+
"""List registered models."""
|
|
121
|
+
self.initialize()
|
|
122
|
+
filter_str = f"name = '{name}'" if name else None
|
|
123
|
+
|
|
124
|
+
models = self._client.search_registered_models(filter_string=filter_str)
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
"name": m.name,
|
|
128
|
+
"latest_versions": [v.version for v in m.latest_versions],
|
|
129
|
+
"creation_timestamp": m.creation_timestamp,
|
|
130
|
+
}
|
|
131
|
+
for m in models
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
@trace_execution(operation_name="mlflow_transition_stage")
|
|
135
|
+
def transition_model_stage(
|
|
136
|
+
self,
|
|
137
|
+
name: str,
|
|
138
|
+
version: str,
|
|
139
|
+
stage: str,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Transition model to stage (Staging, Production, Archived)."""
|
|
142
|
+
self.initialize()
|
|
143
|
+
|
|
144
|
+
# Map generic stages to MLflow specific if needed, but they are usually compatible
|
|
145
|
+
# MLflow: "Staging", "Production", "Archived", "None"
|
|
146
|
+
|
|
147
|
+
valid_stages = ["Staging", "Production", "Archived", "None"]
|
|
148
|
+
target_stage = stage.capitalize()
|
|
149
|
+
|
|
150
|
+
if target_stage not in valid_stages:
|
|
151
|
+
logger.warning(f"Stage '{stage}' might not be valid. Valid: {valid_stages}")
|
|
152
|
+
|
|
153
|
+
self._client.transition_model_version_stage(
|
|
154
|
+
name=name,
|
|
155
|
+
version=version,
|
|
156
|
+
stage=target_stage,
|
|
157
|
+
archive_existing_versions=True, # Standard practice
|
|
158
|
+
)
|
|
159
|
+
logger.info(f"Transitioned {name} v{version} to {target_stage}")
|