flowyml 1.1.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 (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,78 @@
1
+ """Plugin Configuration System.
2
+
3
+ This module handles the configuration of plugins and adaptation rules,
4
+ allowing users to define how external components map to flowyml.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+ import yaml
10
+ from pathlib import Path
11
+
12
+ from flowyml.stacks.components import ComponentType
13
+ from flowyml.stacks.bridge import AdaptationRule
14
+
15
+
16
+ @dataclass
17
+ class PluginConfig:
18
+ """Configuration for a single plugin."""
19
+
20
+ name: str
21
+ source: str # Import path or bridge URI
22
+ component_type: str = "orchestrator" # orchestrator, artifact_store, etc.
23
+ adaptation: dict[str, Any] = field(default_factory=dict)
24
+
25
+
26
+ class PluginManager:
27
+ """Manages plugin configurations and rules."""
28
+
29
+ def __init__(self):
30
+ self.configs: list[PluginConfig] = []
31
+
32
+ def load_from_yaml(self, path: str) -> None:
33
+ """Load plugin configurations from a YAML file."""
34
+ path_obj = Path(path)
35
+ if not path_obj.exists():
36
+ return
37
+
38
+ with open(path) as f:
39
+ data = yaml.safe_load(f)
40
+
41
+ if not data or "plugins" not in data:
42
+ return
43
+
44
+ for p_data in data["plugins"]:
45
+ self.configs.append(
46
+ PluginConfig(
47
+ name=p_data["name"],
48
+ source=p_data["source"],
49
+ component_type=p_data.get("type", "orchestrator"),
50
+ adaptation=p_data.get("adaptation", {}),
51
+ ),
52
+ )
53
+
54
+ def get_adaptation_rules(self) -> list[AdaptationRule]:
55
+ """Convert configurations to adaptation rules."""
56
+ rules = []
57
+ for config in self.configs:
58
+ # Determine target type
59
+ target_type = ComponentType.ORCHESTRATOR
60
+ if config.component_type == "artifact_store":
61
+ target_type = ComponentType.ARTIFACT_STORE
62
+ elif config.component_type == "container_registry":
63
+ target_type = ComponentType.CONTAINER_REGISTRY
64
+
65
+ # Extract mapping
66
+ method_mapping = config.adaptation.get("method_mapping", {})
67
+ attribute_mapping = config.adaptation.get("attribute_mapping", {})
68
+
69
+ # Create rule
70
+ rule = AdaptationRule(
71
+ source_type=config.source,
72
+ target_type=target_type,
73
+ method_mapping=method_mapping,
74
+ attribute_mapping=attribute_mapping,
75
+ )
76
+ rules.append(rule)
77
+
78
+ return rules
@@ -0,0 +1,401 @@
1
+ """Plugin System for Custom Stack Components.
2
+
3
+ This module provides a robust plugin system that allows users to:
4
+ 1. Discover and install plugins from various sources (PyPI, Local)
5
+ 2. Manage plugin lifecycles (install, update, remove)
6
+ 3. Auto-discover components via entry points
7
+ 4. Seamlessly integrate components from other ecosystems via Generic Bridges
8
+ """
9
+
10
+ import importlib
11
+ import importlib.util
12
+ import importlib.metadata
13
+ import inspect
14
+ import subprocess
15
+ import sys
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Optional, Protocol, runtime_checkable
18
+
19
+ from flowyml.stacks.components import (
20
+ StackComponent,
21
+ Orchestrator,
22
+ ArtifactStore,
23
+ ContainerRegistry,
24
+ ComponentType,
25
+ )
26
+ from flowyml.stacks.bridge import GenericBridge, AdaptationRule
27
+
28
+
29
+ @dataclass
30
+ class PluginInfo:
31
+ """Metadata about a plugin."""
32
+
33
+ name: str
34
+ version: str
35
+ description: str = ""
36
+ author: str = ""
37
+ is_installed: bool = False
38
+ source: str = "pypi" # "pypi", "local", "github"
39
+ dependencies: list[str] = field(default_factory=list)
40
+ components: list[str] = field(default_factory=list)
41
+
42
+
43
+ @runtime_checkable
44
+ class PluginBridge(Protocol):
45
+ """Protocol for plugin bridges that adapt external components."""
46
+
47
+ def wrap_component(
48
+ self,
49
+ component_class: Any,
50
+ name: str,
51
+ config: Optional[dict[str, Any]] = None,
52
+ ) -> type[StackComponent]:
53
+ """Wrap an external component class into a flowyml component."""
54
+ ...
55
+
56
+ def is_available(self) -> bool:
57
+ """Check if the external system is available."""
58
+ ...
59
+
60
+
61
+ class ComponentRegistry:
62
+ """Registry for stack component plugins.
63
+
64
+ Supports:
65
+ - Auto-discovery via entry points
66
+ - Manual registration
67
+ - Generic Bridge system for external integrations
68
+ - Plugin management (install/uninstall)
69
+ """
70
+
71
+ def __init__(self):
72
+ self._orchestrators: dict[str, type[Orchestrator]] = {}
73
+ self._artifact_stores: dict[str, type[ArtifactStore]] = {}
74
+ self._container_registries: dict[str, type[ContainerRegistry]] = {}
75
+ self._custom_components: dict[str, type[StackComponent]] = {}
76
+ self._plugins: dict[str, PluginInfo] = {}
77
+ self._bridges: dict[str, PluginBridge] = {}
78
+
79
+ # Register default generic bridge
80
+ self._register_default_bridges()
81
+
82
+ # Auto-discover plugins
83
+ self._discover_installed_plugins()
84
+
85
+ def _register_default_bridges(self) -> None:
86
+ """Register default bridges."""
87
+ # Register a generic bridge that can handle ZenML components via rules
88
+ zenml_rules = [
89
+ AdaptationRule(
90
+ source_type="zenml.orchestrators.base.BaseOrchestrator",
91
+ target_type=ComponentType.ORCHESTRATOR,
92
+ method_mapping={"run_pipeline": "run"},
93
+ ),
94
+ AdaptationRule(
95
+ source_type="zenml.artifact_stores.base_artifact_store.BaseArtifactStore",
96
+ target_type=ComponentType.ARTIFACT_STORE,
97
+ ),
98
+ AdaptationRule(
99
+ source_type="zenml.container_registries.base_container_registry.BaseContainerRegistry",
100
+ target_type=ComponentType.CONTAINER_REGISTRY,
101
+ ),
102
+ # Fallback rule for anything named *Orchestrator
103
+ AdaptationRule(
104
+ name_pattern=".*Orchestrator",
105
+ target_type=ComponentType.ORCHESTRATOR,
106
+ ),
107
+ ]
108
+ self.register_bridge("zenml", GenericBridge(rules=zenml_rules))
109
+
110
+ # We can add other default bridges here (e.g. Airflow) without hard deps
111
+
112
+ def register_bridge(self, prefix: str, bridge: PluginBridge) -> None:
113
+ """Register a bridge for a specific URI prefix (e.g., 'zenml:', 'airflow:')."""
114
+ self._bridges[prefix] = bridge
115
+
116
+ def _discover_installed_plugins(self) -> None:
117
+ """Auto-discover installed plugins via entry points."""
118
+ try:
119
+ # Discover flowyml plugins
120
+ eps = importlib.metadata.entry_points()
121
+ if hasattr(eps, "select"):
122
+ flowyml_eps = eps.select(group="flowyml.stack_components")
123
+ else:
124
+ flowyml_eps = eps.get("flowyml.stack_components", [])
125
+
126
+ for ep in flowyml_eps:
127
+ try:
128
+ component_class = ep.load()
129
+ self.register(component_class)
130
+
131
+ # Record plugin info
132
+ dist = importlib.metadata.distribution(ep.dist.name)
133
+ self._plugins[ep.dist.name] = PluginInfo(
134
+ name=ep.dist.name,
135
+ version=dist.version,
136
+ description=dist.metadata.get("Summary", ""),
137
+ author=dist.metadata.get("Author", ""),
138
+ is_installed=True,
139
+ source="pypi",
140
+ components=[ep.name],
141
+ )
142
+ except Exception:
143
+ pass
144
+
145
+ # Discover Bridges
146
+ if hasattr(eps, "select"):
147
+ bridge_eps = eps.select(group="flowyml.bridges")
148
+ else:
149
+ bridge_eps = eps.get("flowyml.bridges", [])
150
+
151
+ for ep in bridge_eps:
152
+ try:
153
+ bridge_class = ep.load()
154
+ self.register_bridge(ep.name, bridge_class())
155
+ except Exception:
156
+ pass
157
+
158
+ except Exception:
159
+ # Entry points not available or error in discovery
160
+ pass
161
+
162
+ def register(self, component_class: type[StackComponent], name: str | None = None) -> None:
163
+ """Register a stack component.
164
+
165
+ Args:
166
+ component_class: Component class to register
167
+ name: Optional name override. If None, uses class name in snake_case
168
+ """
169
+ if name is None:
170
+ name = self._class_to_snake_case(component_class.__name__)
171
+
172
+ # Determine component type and register
173
+ if issubclass(component_class, Orchestrator):
174
+ self._orchestrators[name] = component_class
175
+ elif issubclass(component_class, ArtifactStore):
176
+ self._artifact_stores[name] = component_class
177
+ elif issubclass(component_class, ContainerRegistry):
178
+ self._container_registries[name] = component_class
179
+ else:
180
+ self._custom_components[name] = component_class
181
+
182
+ def get_orchestrator(self, name: str) -> type[Orchestrator] | None:
183
+ """Get orchestrator class by name."""
184
+ return self._orchestrators.get(name)
185
+
186
+ def get_artifact_store(self, name: str) -> type[ArtifactStore] | None:
187
+ """Get artifact store class by name."""
188
+ return self._artifact_stores.get(name)
189
+
190
+ def get_container_registry(self, name: str) -> type[ContainerRegistry] | None:
191
+ """Get container registry class by name."""
192
+ return self._container_registries.get(name)
193
+
194
+ def get_component(self, name: str) -> type[StackComponent] | None:
195
+ """Get any component by name."""
196
+ return (
197
+ self._orchestrators.get(name)
198
+ or self._artifact_stores.get(name)
199
+ or self._container_registries.get(name)
200
+ or self._custom_components.get(name)
201
+ )
202
+
203
+ def list_orchestrators(self) -> list[str]:
204
+ """List all registered orchestrators."""
205
+ return list(self._orchestrators.keys())
206
+
207
+ def list_artifact_stores(self) -> list[str]:
208
+ """List all registered artifact stores."""
209
+ return list(self._artifact_stores.keys())
210
+
211
+ def list_container_registries(self) -> list[str]:
212
+ """List all registered container registries."""
213
+ return list(self._container_registries.keys())
214
+
215
+ def list_all(self) -> dict[str, list[str]]:
216
+ """List all registered components."""
217
+ return {
218
+ "orchestrators": self.list_orchestrators(),
219
+ "artifact_stores": self.list_artifact_stores(),
220
+ "container_registries": self.list_container_registries(),
221
+ "custom": list(self._custom_components.keys()),
222
+ }
223
+
224
+ def list_plugins(self) -> list[PluginInfo]:
225
+ """List all discovered plugins."""
226
+ return list(self._plugins.values())
227
+
228
+ def list_bridges(self) -> list[str]:
229
+ """List all registered bridges."""
230
+ return list(self._bridges.keys())
231
+
232
+ def load_from_path(self, path: str, class_name: str) -> None:
233
+ """Load a component from a Python file."""
234
+ spec = importlib.util.spec_from_file_location("custom_module", path)
235
+ if spec is None or spec.loader is None:
236
+ raise ImportError(f"Could not load module from {path}")
237
+
238
+ module = importlib.util.module_from_spec(spec)
239
+ spec.loader.exec_module(module)
240
+
241
+ component_class = getattr(module, class_name)
242
+ self.register(component_class)
243
+
244
+ def load_from_module(self, module_path: str) -> None:
245
+ """Load all components from a module."""
246
+ module = importlib.import_module(module_path)
247
+
248
+ for _name, obj in inspect.getmembers(module, inspect.isclass):
249
+ if issubclass(obj, StackComponent) and obj != StackComponent:
250
+ self.register(obj)
251
+
252
+ def load_via_bridge(self, bridge_name: str, component_path: str, name: str | None = None) -> None:
253
+ """Load a component via a registered bridge."""
254
+ bridge = self._bridges.get(bridge_name)
255
+ if not bridge:
256
+ raise ValueError(f"Bridge '{bridge_name}' not found. Available: {list(self._bridges.keys())}")
257
+
258
+ module_path, class_name = component_path.rsplit(".", 1)
259
+
260
+ try:
261
+ module = importlib.import_module(module_path)
262
+ component_class = getattr(module, class_name)
263
+
264
+ wrapper = bridge.wrap_component(component_class, name or class_name)
265
+ self.register(wrapper, name or class_name)
266
+ except ImportError as e:
267
+ raise ImportError(f"Could not import component via {bridge_name}: {e}")
268
+
269
+ def load_plugins_from_config(self, config_path: str) -> None:
270
+ """Load plugins from a configuration file."""
271
+ from flowyml.stacks.plugin_config import PluginManager
272
+
273
+ manager = PluginManager()
274
+ manager.load_from_yaml(config_path)
275
+
276
+ # Get generic bridge (or create one if not exists)
277
+ bridge = self._bridges.get("zenml") # Use the default generic bridge
278
+ if not bridge or not isinstance(bridge, GenericBridge):
279
+ # Create a fresh generic bridge if needed
280
+ bridge = GenericBridge()
281
+ self.register_bridge("generic", bridge)
282
+
283
+ # Register components from config
284
+ for config in manager.configs:
285
+ # Create specific rule for this component
286
+ # Note: In a real implementation, we might want to merge rules or handle them more globally
287
+ # For now, we'll just use the bridge to wrap it
288
+
289
+ try:
290
+ # Import the source class
291
+ module_path, class_name = config.source.rsplit(".", 1)
292
+ module = importlib.import_module(module_path)
293
+ component_class = getattr(module, class_name)
294
+
295
+ # Create rule from config
296
+ # We need to add this rule to the bridge so it knows how to handle it
297
+ # Or we can pass the rule directly to wrap_component if we modify GenericBridge
298
+ # But GenericBridge uses internal rules.
299
+ # Let's add the rule to the bridge
300
+
301
+ # Convert config to rule (simplified)
302
+ target_type = ComponentType.ORCHESTRATOR
303
+ if config.component_type == "artifact_store":
304
+ target_type = ComponentType.ARTIFACT_STORE
305
+ elif config.component_type == "container_registry":
306
+ target_type = ComponentType.CONTAINER_REGISTRY
307
+
308
+ rule = AdaptationRule(
309
+ source_type=config.source,
310
+ target_type=target_type,
311
+ method_mapping=config.adaptation.get("method_mapping", {}),
312
+ attribute_mapping=config.adaptation.get("attribute_mapping", {}),
313
+ )
314
+
315
+ # Add rule to bridge
316
+ if isinstance(bridge, GenericBridge):
317
+ bridge.rules.append(rule)
318
+
319
+ # Wrap and register
320
+ wrapper = bridge.wrap_component(component_class, config.name)
321
+ self.register(wrapper, config.name)
322
+
323
+ except Exception as e:
324
+ print(f"Failed to load plugin {config.name}: {e}")
325
+
326
+ def install_plugin(self, package_name: str) -> bool:
327
+ """Install a plugin package via pip."""
328
+ try:
329
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
330
+ # Refresh discovery
331
+ importlib.invalidate_caches()
332
+ self._discover_installed_plugins()
333
+ return True
334
+ except subprocess.CalledProcessError:
335
+ return False
336
+
337
+ @staticmethod
338
+ def _class_to_snake_case(name: str) -> str:
339
+ """Convert ClassName to class_name."""
340
+ import re
341
+
342
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
343
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
344
+
345
+
346
+ # Global registry instance
347
+ _global_component_registry: ComponentRegistry | None = None
348
+
349
+
350
+ def get_component_registry() -> ComponentRegistry:
351
+ """Get the global component registry."""
352
+ global _global_component_registry
353
+ if _global_component_registry is None:
354
+ _global_component_registry = ComponentRegistry()
355
+ return _global_component_registry
356
+
357
+
358
+ def register_component(component_class=None, name: str | None = None):
359
+ """Decorator to register a custom component."""
360
+
361
+ def wrapper(cls):
362
+ get_component_registry().register(cls, name)
363
+ return cls
364
+
365
+ if component_class is not None:
366
+ get_component_registry().register(component_class, name)
367
+ return component_class
368
+
369
+ return wrapper
370
+
371
+
372
+ def load_component(source: str, name: str | None = None) -> None:
373
+ """Load a component from various sources.
374
+
375
+ Args:
376
+ source: Can be:
377
+ - Module path: "my_package.components"
378
+ - File path: "/path/to/component.py:ClassName"
379
+ - Bridge URI: "zenml:zenml.integrations.kubernetes.orchestrators.KubernetesOrchestrator"
380
+ - Bridge URI: "airflow:airflow.providers.google.cloud.operators.bigquery.BigQueryExecuteQueryOperator"
381
+ name: Optional name to register as
382
+ """
383
+ registry = get_component_registry()
384
+
385
+ # Check for bridge prefixes (e.g., "zenml:", "airflow:")
386
+ if ":" in source and not source.startswith("/") and not source.startswith("."):
387
+ prefix, path = source.split(":", 1)
388
+
389
+ # If prefix is a registered bridge, use it
390
+ if prefix in registry.list_bridges():
391
+ registry.load_via_bridge(prefix, path, name)
392
+ return
393
+
394
+ # Special case for file paths that might look like "c:/path" on windows or just "file:Class"
395
+ # But here we assume "file_path:ClassName" for local loading
396
+ if ".py" in prefix:
397
+ registry.load_from_path(prefix, path)
398
+ return
399
+
400
+ # Fallback to module load
401
+ registry.load_from_module(source)
@@ -0,0 +1,226 @@
1
+ """Stack Registry - Manage and switch between different stacks.
2
+
3
+ This module provides a registry for storing, loading, and switching
4
+ between different infrastructure stacks.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from flowyml.stacks.base import Stack
11
+
12
+
13
+ class StackRegistry:
14
+ """Registry for managing multiple stacks.
15
+
16
+ The registry allows you to:
17
+ - Register stacks with unique names
18
+ - Switch between stacks seamlessly
19
+ - Save and load stack configurations
20
+ - List available stacks
21
+
22
+ Example:
23
+ ```python
24
+ from flowyml.stacks import StackRegistry, LocalStack
25
+ from flowyml.stacks.gcp import GCPStack
26
+
27
+ # Initialize registry
28
+ registry = StackRegistry()
29
+
30
+ # Register stacks
31
+ local_stack = LocalStack(name="local")
32
+ registry.register_stack(local_stack)
33
+
34
+ gcp_stack = GCPStack(name="production", project_id="my-project", bucket_name="my-artifacts")
35
+ registry.register_stack(gcp_stack)
36
+
37
+ # Switch stacks
38
+ registry.set_active_stack("local") # For development
39
+ registry.set_active_stack("production") # For production
40
+
41
+ # Get active stack
42
+ active = registry.get_active_stack()
43
+ ```
44
+ """
45
+
46
+ def __init__(self, config_path: str | None = None):
47
+ """Initialize stack registry.
48
+
49
+ Args:
50
+ config_path: Path to store stack configurations
51
+ """
52
+ self.config_path = Path(config_path or ".flowyml/stacks.json")
53
+ self.stacks: dict[str, Stack] = {}
54
+ self.active_stack_name: str | None = None
55
+
56
+ # Create config directory
57
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
58
+
59
+ # Load existing configurations
60
+ self.load()
61
+
62
+ def register_stack(self, stack: Stack, set_active: bool = False) -> None:
63
+ """Register a stack in the registry.
64
+
65
+ Args:
66
+ stack: Stack instance to register
67
+ set_active: Whether to set this as the active stack
68
+ """
69
+ self.stacks[stack.name] = stack
70
+
71
+ if set_active or self.active_stack_name is None:
72
+ self.active_stack_name = stack.name
73
+
74
+ # Save configuration
75
+ self.save()
76
+
77
+ def unregister_stack(self, name: str) -> None:
78
+ """Remove a stack from the registry.
79
+
80
+ Args:
81
+ name: Name of the stack to remove
82
+ """
83
+ if name in self.stacks:
84
+ del self.stacks[name]
85
+
86
+ # If this was the active stack, clear it
87
+ if self.active_stack_name == name:
88
+ self.active_stack_name = list(self.stacks.keys())[0] if self.stacks else None
89
+
90
+ self.save()
91
+
92
+ def get_stack(self, name: str) -> Stack | None:
93
+ """Get a specific stack by name.
94
+
95
+ Args:
96
+ name: Stack name
97
+
98
+ Returns:
99
+ Stack instance or None
100
+ """
101
+ return self.stacks.get(name)
102
+
103
+ def get_active_stack(self) -> Stack | None:
104
+ """Get the currently active stack.
105
+
106
+ Returns:
107
+ Active stack instance or None
108
+ """
109
+ if self.active_stack_name:
110
+ return self.stacks.get(self.active_stack_name)
111
+ return None
112
+
113
+ def set_active_stack(self, name: str) -> None:
114
+ """Set the active stack.
115
+
116
+ Args:
117
+ name: Name of the stack to activate
118
+ """
119
+ if name not in self.stacks:
120
+ raise ValueError(f"Stack '{name}' not found in registry")
121
+
122
+ self.active_stack_name = name
123
+ self.save()
124
+
125
+ def list_stacks(self) -> list[str]:
126
+ """List all registered stacks.
127
+
128
+ Returns:
129
+ List of stack names
130
+ """
131
+ return list(self.stacks.keys())
132
+
133
+ def describe_stack(self, name: str) -> dict[str, Any]:
134
+ """Get detailed information about a stack.
135
+
136
+ Args:
137
+ name: Stack name
138
+
139
+ Returns:
140
+ Dictionary with stack details
141
+ """
142
+ stack = self.get_stack(name)
143
+ if not stack:
144
+ raise ValueError(f"Stack '{name}' not found")
145
+
146
+ return {
147
+ "name": stack.name,
148
+ "is_active": name == self.active_stack_name,
149
+ "config": stack.config.to_dict(),
150
+ }
151
+
152
+ def save(self) -> None:
153
+ """Save stack configurations to disk."""
154
+ config = {
155
+ "active_stack": self.active_stack_name,
156
+ "stacks": {name: self._serialize_stack(stack) for name, stack in self.stacks.items()},
157
+ }
158
+
159
+ with open(self.config_path, "w") as f:
160
+ json.dump(config, f, indent=2)
161
+
162
+ def load(self) -> None:
163
+ """Load stack configurations from disk."""
164
+ if not self.config_path.exists():
165
+ return
166
+
167
+ try:
168
+ with open(self.config_path) as f:
169
+ config = json.load(f)
170
+
171
+ self.active_stack_name = config.get("active_stack")
172
+
173
+ # Note: In a full implementation, we would deserialize
174
+ # and recreate stack objects here. For now, stacks must
175
+ # be registered programmatically.
176
+
177
+ except Exception:
178
+ pass
179
+
180
+ def _serialize_stack(self, stack: Stack) -> dict[str, Any]:
181
+ """Serialize a stack to dictionary."""
182
+ return stack.config.to_dict()
183
+
184
+ def clear(self) -> None:
185
+ """Clear all registered stacks."""
186
+ self.stacks.clear()
187
+ self.active_stack_name = None
188
+ self.save()
189
+
190
+ def __repr__(self) -> str:
191
+ active = f" (active: {self.active_stack_name})" if self.active_stack_name else ""
192
+ return f"StackRegistry({len(self.stacks)} stacks{active})"
193
+
194
+
195
+ # Global registry instance
196
+ _global_registry: StackRegistry | None = None
197
+
198
+
199
+ def get_registry() -> StackRegistry:
200
+ """Get the global stack registry instance.
201
+
202
+ Returns:
203
+ Global StackRegistry instance
204
+ """
205
+ global _global_registry
206
+ if _global_registry is None:
207
+ _global_registry = StackRegistry()
208
+ return _global_registry
209
+
210
+
211
+ def get_active_stack() -> Stack | None:
212
+ """Get the currently active stack from the global registry.
213
+
214
+ Returns:
215
+ Active stack or None
216
+ """
217
+ return get_registry().get_active_stack()
218
+
219
+
220
+ def set_active_stack(name: str) -> None:
221
+ """Set the active stack in the global registry.
222
+
223
+ Args:
224
+ name: Name of the stack to activate
225
+ """
226
+ get_registry().set_active_stack(name)