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.
Files changed (137) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/dataset.py +570 -17
  3. flowyml/assets/metrics.py +5 -0
  4. flowyml/assets/model.py +1052 -15
  5. flowyml/cli/main.py +709 -0
  6. flowyml/cli/stack_cli.py +138 -25
  7. flowyml/core/__init__.py +17 -0
  8. flowyml/core/executor.py +231 -37
  9. flowyml/core/image_builder.py +129 -0
  10. flowyml/core/log_streamer.py +227 -0
  11. flowyml/core/orchestrator.py +59 -4
  12. flowyml/core/pipeline.py +65 -13
  13. flowyml/core/routing.py +558 -0
  14. flowyml/core/scheduler.py +88 -5
  15. flowyml/core/step.py +9 -1
  16. flowyml/core/step_grouping.py +49 -35
  17. flowyml/core/types.py +407 -0
  18. flowyml/integrations/keras.py +247 -82
  19. flowyml/monitoring/alerts.py +10 -0
  20. flowyml/monitoring/notifications.py +104 -25
  21. flowyml/monitoring/slack_blocks.py +323 -0
  22. flowyml/plugins/__init__.py +251 -0
  23. flowyml/plugins/alerters/__init__.py +1 -0
  24. flowyml/plugins/alerters/slack.py +168 -0
  25. flowyml/plugins/base.py +752 -0
  26. flowyml/plugins/config.py +478 -0
  27. flowyml/plugins/deployers/__init__.py +22 -0
  28. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  29. flowyml/plugins/deployers/sagemaker.py +306 -0
  30. flowyml/plugins/deployers/vertex.py +290 -0
  31. flowyml/plugins/integration.py +369 -0
  32. flowyml/plugins/manager.py +510 -0
  33. flowyml/plugins/model_registries/__init__.py +22 -0
  34. flowyml/plugins/model_registries/mlflow.py +159 -0
  35. flowyml/plugins/model_registries/sagemaker.py +489 -0
  36. flowyml/plugins/model_registries/vertex.py +386 -0
  37. flowyml/plugins/orchestrators/__init__.py +13 -0
  38. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  39. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  40. flowyml/plugins/registries/__init__.py +13 -0
  41. flowyml/plugins/registries/ecr.py +321 -0
  42. flowyml/plugins/registries/gcr.py +313 -0
  43. flowyml/plugins/registry.py +454 -0
  44. flowyml/plugins/stack.py +494 -0
  45. flowyml/plugins/stack_config.py +537 -0
  46. flowyml/plugins/stores/__init__.py +13 -0
  47. flowyml/plugins/stores/gcs.py +460 -0
  48. flowyml/plugins/stores/s3.py +453 -0
  49. flowyml/plugins/trackers/__init__.py +11 -0
  50. flowyml/plugins/trackers/mlflow.py +316 -0
  51. flowyml/plugins/validators/__init__.py +3 -0
  52. flowyml/plugins/validators/deepchecks.py +119 -0
  53. flowyml/registry/__init__.py +2 -1
  54. flowyml/registry/model_environment.py +109 -0
  55. flowyml/registry/model_registry.py +241 -96
  56. flowyml/serving/__init__.py +17 -0
  57. flowyml/serving/model_server.py +628 -0
  58. flowyml/stacks/__init__.py +60 -0
  59. flowyml/stacks/aws.py +93 -0
  60. flowyml/stacks/base.py +62 -0
  61. flowyml/stacks/components.py +12 -0
  62. flowyml/stacks/gcp.py +44 -9
  63. flowyml/stacks/plugins.py +115 -0
  64. flowyml/stacks/registry.py +2 -1
  65. flowyml/storage/sql.py +401 -12
  66. flowyml/tracking/experiment.py +8 -5
  67. flowyml/ui/backend/Dockerfile +87 -16
  68. flowyml/ui/backend/auth.py +12 -2
  69. flowyml/ui/backend/main.py +149 -5
  70. flowyml/ui/backend/routers/ai_context.py +226 -0
  71. flowyml/ui/backend/routers/assets.py +23 -4
  72. flowyml/ui/backend/routers/auth.py +96 -0
  73. flowyml/ui/backend/routers/deployments.py +660 -0
  74. flowyml/ui/backend/routers/model_explorer.py +597 -0
  75. flowyml/ui/backend/routers/plugins.py +103 -51
  76. flowyml/ui/backend/routers/projects.py +91 -8
  77. flowyml/ui/backend/routers/runs.py +132 -1
  78. flowyml/ui/backend/routers/schedules.py +54 -29
  79. flowyml/ui/backend/routers/templates.py +319 -0
  80. flowyml/ui/backend/routers/websocket.py +2 -2
  81. flowyml/ui/frontend/Dockerfile +55 -6
  82. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  83. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  84. flowyml/ui/frontend/dist/index.html +2 -2
  85. flowyml/ui/frontend/dist/logo.png +0 -0
  86. flowyml/ui/frontend/nginx.conf +65 -4
  87. flowyml/ui/frontend/package-lock.json +1415 -74
  88. flowyml/ui/frontend/package.json +4 -0
  89. flowyml/ui/frontend/public/logo.png +0 -0
  90. flowyml/ui/frontend/src/App.jsx +10 -7
  91. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  92. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  93. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  94. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  95. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  96. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  97. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  98. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  99. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
  100. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  101. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  102. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  103. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
  104. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  105. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  106. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  107. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  108. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  109. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  110. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  111. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  112. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  113. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  114. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  115. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  116. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  117. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  118. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  119. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  120. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  121. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  122. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  123. flowyml/ui/frontend/src/router/index.jsx +47 -20
  124. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  125. flowyml/ui/server_manager.py +5 -5
  126. flowyml/ui/utils.py +157 -39
  127. flowyml/utils/config.py +37 -15
  128. flowyml/utils/model_introspection.py +123 -0
  129. flowyml/utils/observability.py +30 -0
  130. flowyml-1.8.0.dist-info/METADATA +174 -0
  131. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
  132. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  133. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  134. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  135. flowyml-1.7.1.dist-info/METADATA +0 -477
  136. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  137. {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}")