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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- 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)
|