flowyml 1.7.2__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/assets/base.py +15 -0
- flowyml/assets/metrics.py +5 -0
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +161 -26
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +22 -2
- flowyml/core/pipeline.py +34 -10
- flowyml/core/routing.py +558 -0
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +20 -1
- flowyml/ui/backend/routers/schedules.py +22 -17
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1404 -74
- flowyml/ui/frontend/package.json +3 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
- flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
- flowyml-1.7.2.dist-info/METADATA +0 -477
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""FlowyML Stack Configuration - Multi-Stack Support with Type-Based Routing.
|
|
2
|
+
|
|
3
|
+
This module extends the plugin configuration to support:
|
|
4
|
+
1. Multiple named stacks in a single config file
|
|
5
|
+
2. Type-based artifact routing (Model → registry, Dataset → store, etc.)
|
|
6
|
+
3. Stack switching via environment variable or code
|
|
7
|
+
4. Path templating for artifacts
|
|
8
|
+
|
|
9
|
+
Example flowyml.yaml:
|
|
10
|
+
stacks:
|
|
11
|
+
local:
|
|
12
|
+
orchestrator: { type: local }
|
|
13
|
+
artifact_store: { type: local, path: "./artifacts" }
|
|
14
|
+
|
|
15
|
+
gcp-prod:
|
|
16
|
+
orchestrator: { type: vertex_ai, project: ${GCP_PROJECT} }
|
|
17
|
+
artifact_routing:
|
|
18
|
+
Model: { store: gcs, register: true }
|
|
19
|
+
model_registry: { type: vertex_model_registry }
|
|
20
|
+
model_deployer: { type: vertex_endpoints }
|
|
21
|
+
|
|
22
|
+
aws-staging:
|
|
23
|
+
orchestrator: { type: sagemaker, region: us-east-1 }
|
|
24
|
+
artifact_routing:
|
|
25
|
+
Model: { store: s3, register: true }
|
|
26
|
+
model_registry: { type: sagemaker_model_registry }
|
|
27
|
+
|
|
28
|
+
active_stack: local # Default stack
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
from flowyml.plugins.stack_config import get_active_stack, use_stack
|
|
32
|
+
|
|
33
|
+
# Get current stack
|
|
34
|
+
stack = get_active_stack()
|
|
35
|
+
|
|
36
|
+
# Switch stack temporarily
|
|
37
|
+
with use_stack("gcp-prod"):
|
|
38
|
+
pipeline.run()
|
|
39
|
+
|
|
40
|
+
# Or via environment variable
|
|
41
|
+
# FLOWYML_STACK=gcp-prod flowyml run my_pipeline
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import os
|
|
45
|
+
import logging
|
|
46
|
+
from contextlib import contextmanager
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
from typing import Any, Optional
|
|
49
|
+
from collections.abc import Callable
|
|
50
|
+
|
|
51
|
+
from flowyml.plugins.config import get_config, PluginConfig
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# ARTIFACT ROUTING CONFIGURATION
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ArtifactRoutingRule:
|
|
63
|
+
"""Configuration for routing a specific artifact type.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
store: Name of the artifact store to use (e.g., "gcs", "s3", "local")
|
|
67
|
+
path: Path template for the artifact (supports {run_id}, {step_name})
|
|
68
|
+
register: Whether to register the artifact (e.g., models to registry)
|
|
69
|
+
deploy: Whether deployment is enabled (still requires approval or condition)
|
|
70
|
+
deploy_condition: Condition for auto-deploy ("manual", "auto", "on_approval")
|
|
71
|
+
deploy_min_metrics: Minimum metrics required for deployment (e.g., {"accuracy": 0.9})
|
|
72
|
+
endpoint_name: Optional endpoint name for deployment
|
|
73
|
+
log_to_tracker: Whether to log to experiment tracker (e.g., for Metrics)
|
|
74
|
+
metadata: Additional metadata to attach
|
|
75
|
+
|
|
76
|
+
Deployment Modes:
|
|
77
|
+
- deploy=False: Never deploy
|
|
78
|
+
- deploy=True, deploy_condition="manual": Register only, deploy via CLI/UI
|
|
79
|
+
- deploy=True, deploy_condition="on_approval": Wait for human approval
|
|
80
|
+
- deploy=True, deploy_condition="auto": Deploy if metrics meet thresholds
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
store: str | None = None
|
|
84
|
+
path: str = "{run_id}/{step_name}/{artifact_name}"
|
|
85
|
+
register: bool = False
|
|
86
|
+
deploy: bool = False
|
|
87
|
+
deploy_condition: str = "manual" # "manual", "auto", "on_approval"
|
|
88
|
+
deploy_min_metrics: dict[str, float] = field(default_factory=dict)
|
|
89
|
+
endpoint_name: str | None = None
|
|
90
|
+
log_to_tracker: bool = False
|
|
91
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict[str, Any]) -> "ArtifactRoutingRule":
|
|
95
|
+
"""Create from dictionary."""
|
|
96
|
+
return cls(
|
|
97
|
+
store=data.get("store"),
|
|
98
|
+
path=data.get("path", "{run_id}/{step_name}/{artifact_name}"),
|
|
99
|
+
register=data.get("register", False),
|
|
100
|
+
deploy=data.get("deploy", False),
|
|
101
|
+
deploy_condition=data.get("deploy_condition", "manual"),
|
|
102
|
+
deploy_min_metrics=data.get("deploy_min_metrics", {}),
|
|
103
|
+
endpoint_name=data.get("endpoint_name"),
|
|
104
|
+
log_to_tracker=data.get("log_to_tracker", False),
|
|
105
|
+
metadata=data.get("metadata", {}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def should_auto_deploy(self, metrics: dict[str, float] = None) -> bool:
|
|
109
|
+
"""Check if model should be auto-deployed based on condition and metrics.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
metrics: Current model's metrics to compare against thresholds.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if auto-deployment should proceed.
|
|
116
|
+
"""
|
|
117
|
+
if not self.deploy:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
if self.deploy_condition == "manual":
|
|
121
|
+
return False # Requires manual deployment via CLI
|
|
122
|
+
|
|
123
|
+
if self.deploy_condition == "on_approval":
|
|
124
|
+
return False # Requires human approval
|
|
125
|
+
|
|
126
|
+
if self.deploy_condition == "auto":
|
|
127
|
+
# Check if metrics meet minimum thresholds
|
|
128
|
+
if self.deploy_min_metrics and metrics:
|
|
129
|
+
for metric_name, min_value in self.deploy_min_metrics.items():
|
|
130
|
+
if metric_name not in metrics:
|
|
131
|
+
return False
|
|
132
|
+
if metrics[metric_name] < min_value:
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def format_path(
|
|
139
|
+
self,
|
|
140
|
+
run_id: str = "",
|
|
141
|
+
step_name: str = "",
|
|
142
|
+
artifact_name: str = "",
|
|
143
|
+
**kwargs,
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Format the path template with actual values.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
run_id: The run identifier
|
|
149
|
+
step_name: The step name
|
|
150
|
+
artifact_name: The artifact name
|
|
151
|
+
**kwargs: Additional template variables
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Formatted path string.
|
|
155
|
+
"""
|
|
156
|
+
return self.path.format(
|
|
157
|
+
run_id=run_id,
|
|
158
|
+
step_name=step_name,
|
|
159
|
+
artifact_name=artifact_name,
|
|
160
|
+
**kwargs,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class ArtifactRoutingConfig:
|
|
166
|
+
"""Configuration for all artifact type routing.
|
|
167
|
+
|
|
168
|
+
Maps artifact type names (Model, Dataset, Metrics, etc.) to routing rules.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
rules: dict[str, ArtifactRoutingRule] = field(default_factory=dict)
|
|
172
|
+
default: ArtifactRoutingRule | None = None
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def from_dict(cls, data: dict[str, Any]) -> "ArtifactRoutingConfig":
|
|
176
|
+
"""Create from dictionary."""
|
|
177
|
+
rules = {}
|
|
178
|
+
default = None
|
|
179
|
+
|
|
180
|
+
for type_name, rule_data in data.items():
|
|
181
|
+
if type_name == "default":
|
|
182
|
+
default = ArtifactRoutingRule.from_dict(rule_data)
|
|
183
|
+
else:
|
|
184
|
+
rules[type_name] = ArtifactRoutingRule.from_dict(rule_data)
|
|
185
|
+
|
|
186
|
+
return cls(rules=rules, default=default)
|
|
187
|
+
|
|
188
|
+
def get_rule(self, artifact_type: str) -> ArtifactRoutingRule | None:
|
|
189
|
+
"""Get routing rule for an artifact type.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
artifact_type: Name of the artifact type (Model, Dataset, etc.)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Routing rule or default if not found.
|
|
196
|
+
"""
|
|
197
|
+
if artifact_type in self.rules:
|
|
198
|
+
return self.rules[artifact_type]
|
|
199
|
+
return self.default
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# STACK CONFIGURATION
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class StackConfig:
|
|
209
|
+
"""Configuration for a single named stack.
|
|
210
|
+
|
|
211
|
+
A stack is a collection of plugins that work together to run pipelines.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
name: str
|
|
215
|
+
orchestrator: dict[str, Any] | None = None
|
|
216
|
+
artifact_store: dict[str, Any] | None = None
|
|
217
|
+
experiment_tracker: dict[str, Any] | None = None
|
|
218
|
+
model_registry: dict[str, Any] | None = None
|
|
219
|
+
model_deployer: dict[str, Any] | None = None
|
|
220
|
+
container_registry: dict[str, Any] | None = None
|
|
221
|
+
feature_store: dict[str, Any] | None = None
|
|
222
|
+
data_validator: dict[str, Any] | None = None
|
|
223
|
+
alerter: dict[str, Any] | None = None
|
|
224
|
+
artifact_routing: ArtifactRoutingConfig | None = None
|
|
225
|
+
artifact_stores: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def from_dict(cls, name: str, data: dict[str, Any]) -> "StackConfig":
|
|
229
|
+
"""Create from dictionary."""
|
|
230
|
+
# Extract artifact routing
|
|
231
|
+
routing_data = data.get("artifact_routing", {})
|
|
232
|
+
routing = ArtifactRoutingConfig.from_dict(routing_data) if routing_data else None
|
|
233
|
+
|
|
234
|
+
# Extract named artifact stores
|
|
235
|
+
stores = data.get("artifact_stores", {})
|
|
236
|
+
|
|
237
|
+
return cls(
|
|
238
|
+
name=name,
|
|
239
|
+
orchestrator=data.get("orchestrator"),
|
|
240
|
+
artifact_store=data.get("artifact_store"),
|
|
241
|
+
experiment_tracker=data.get("experiment_tracker"),
|
|
242
|
+
model_registry=data.get("model_registry"),
|
|
243
|
+
model_deployer=data.get("model_deployer"),
|
|
244
|
+
container_registry=data.get("container_registry"),
|
|
245
|
+
feature_store=data.get("feature_store"),
|
|
246
|
+
data_validator=data.get("data_validator"),
|
|
247
|
+
alerter=data.get("alerter"),
|
|
248
|
+
artifact_routing=routing,
|
|
249
|
+
artifact_stores=stores,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def get_routing_for_type(self, artifact_type: str) -> ArtifactRoutingRule | None:
|
|
253
|
+
"""Get routing configuration for an artifact type.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
artifact_type: Name of the artifact type.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Routing rule or None.
|
|
260
|
+
"""
|
|
261
|
+
if self.artifact_routing:
|
|
262
|
+
return self.artifact_routing.get_rule(artifact_type)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# MULTI-STACK MANAGER
|
|
268
|
+
# =============================================================================
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class StackManager:
|
|
272
|
+
"""Manages multiple stacks and the active stack.
|
|
273
|
+
|
|
274
|
+
The stack manager:
|
|
275
|
+
1. Loads stack definitions from config
|
|
276
|
+
2. Tracks the active stack
|
|
277
|
+
3. Provides stack switching via context manager
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
_instance: Optional["StackManager"] = None
|
|
281
|
+
|
|
282
|
+
def __init__(self, config: PluginConfig = None):
|
|
283
|
+
"""Initialize the stack manager.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
config: Optional PluginConfig instance.
|
|
287
|
+
"""
|
|
288
|
+
self._config = config or get_config()
|
|
289
|
+
self._stacks: dict[str, StackConfig] = {}
|
|
290
|
+
self._active_stack_name: str | None = None
|
|
291
|
+
self._stack_context: list[str] = [] # Stack for context manager nesting
|
|
292
|
+
|
|
293
|
+
self._load_stacks()
|
|
294
|
+
|
|
295
|
+
def _load_stacks(self) -> None:
|
|
296
|
+
"""Load stack definitions from config."""
|
|
297
|
+
raw_config = self._config._config
|
|
298
|
+
|
|
299
|
+
# Check for stacks section (new format)
|
|
300
|
+
stacks_data = raw_config.get("stacks", {})
|
|
301
|
+
|
|
302
|
+
if stacks_data:
|
|
303
|
+
# New multi-stack format
|
|
304
|
+
for name, stack_data in stacks_data.items():
|
|
305
|
+
self._stacks[name] = StackConfig.from_dict(name, stack_data)
|
|
306
|
+
|
|
307
|
+
# Set active stack from config or env var
|
|
308
|
+
self._active_stack_name = (
|
|
309
|
+
os.environ.get("FLOWYML_STACK")
|
|
310
|
+
or raw_config.get("active_stack")
|
|
311
|
+
or next(iter(self._stacks.keys()), None)
|
|
312
|
+
)
|
|
313
|
+
logger.info(f"Loaded {len(self._stacks)} stacks, active: {self._active_stack_name}")
|
|
314
|
+
else:
|
|
315
|
+
# Legacy single-stack format - create a "default" stack from plugins section
|
|
316
|
+
plugins = raw_config.get("plugins", {})
|
|
317
|
+
if plugins:
|
|
318
|
+
self._stacks["default"] = StackConfig.from_dict("default", plugins)
|
|
319
|
+
self._active_stack_name = "default"
|
|
320
|
+
logger.info("Loaded legacy config as 'default' stack")
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def get_instance(cls, config: PluginConfig = None) -> "StackManager":
|
|
324
|
+
"""Get or create the singleton instance.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
config: Optional PluginConfig to use.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
StackManager instance.
|
|
331
|
+
"""
|
|
332
|
+
if cls._instance is None or config is not None:
|
|
333
|
+
cls._instance = cls(config)
|
|
334
|
+
return cls._instance
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def reset(cls) -> None:
|
|
338
|
+
"""Reset the singleton instance."""
|
|
339
|
+
cls._instance = None
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def active_stack(self) -> StackConfig | None:
|
|
343
|
+
"""Get the currently active stack configuration."""
|
|
344
|
+
if self._active_stack_name:
|
|
345
|
+
return self._stacks.get(self._active_stack_name)
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def active_stack_name(self) -> str | None:
|
|
350
|
+
"""Get the name of the active stack."""
|
|
351
|
+
return self._active_stack_name
|
|
352
|
+
|
|
353
|
+
def list_stacks(self) -> list[str]:
|
|
354
|
+
"""List all available stack names.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
List of stack names.
|
|
358
|
+
"""
|
|
359
|
+
return list(self._stacks.keys())
|
|
360
|
+
|
|
361
|
+
def get_stack(self, name: str) -> StackConfig | None:
|
|
362
|
+
"""Get a stack by name.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
name: Stack name.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Stack configuration or None.
|
|
369
|
+
"""
|
|
370
|
+
return self._stacks.get(name)
|
|
371
|
+
|
|
372
|
+
def set_active_stack(self, name: str) -> bool:
|
|
373
|
+
"""Set the active stack.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
name: Stack name to activate.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
True if successful.
|
|
380
|
+
"""
|
|
381
|
+
if name not in self._stacks:
|
|
382
|
+
logger.error(f"Stack '{name}' not found. Available: {list(self._stacks.keys())}")
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
self._active_stack_name = name
|
|
386
|
+
logger.info(f"Active stack set to: {name}")
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
def register_stack(self, name: str, config: StackConfig) -> None:
|
|
390
|
+
"""Register a new stack configuration.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
name: Stack name.
|
|
394
|
+
config: Stack configuration.
|
|
395
|
+
"""
|
|
396
|
+
self._stacks[name] = config
|
|
397
|
+
logger.info(f"Registered stack: {name}")
|
|
398
|
+
|
|
399
|
+
@contextmanager
|
|
400
|
+
def use_stack(self, name: str):
|
|
401
|
+
"""Context manager for temporarily using a different stack.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
name: Stack name to use.
|
|
405
|
+
|
|
406
|
+
Yields:
|
|
407
|
+
The stack configuration.
|
|
408
|
+
"""
|
|
409
|
+
if name not in self._stacks:
|
|
410
|
+
raise ValueError(f"Stack '{name}' not found. Available: {list(self._stacks.keys())}")
|
|
411
|
+
|
|
412
|
+
# Save current and switch
|
|
413
|
+
previous = self._active_stack_name
|
|
414
|
+
self._stack_context.append(previous)
|
|
415
|
+
self._active_stack_name = name
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
yield self._stacks[name]
|
|
419
|
+
finally:
|
|
420
|
+
# Restore previous
|
|
421
|
+
self._active_stack_name = self._stack_context.pop()
|
|
422
|
+
|
|
423
|
+
def get_routing_for_type(self, artifact_type: str) -> ArtifactRoutingRule | None:
|
|
424
|
+
"""Get artifact routing for a type in the active stack.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
artifact_type: Name of the artifact type.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Routing rule or None.
|
|
431
|
+
"""
|
|
432
|
+
stack = self.active_stack
|
|
433
|
+
if stack:
|
|
434
|
+
return stack.get_routing_for_type(artifact_type)
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# =============================================================================
|
|
439
|
+
# CONVENIENCE FUNCTIONS
|
|
440
|
+
# =============================================================================
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def get_stack_manager(config: PluginConfig = None) -> StackManager:
|
|
444
|
+
"""Get the global stack manager.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
config: Optional PluginConfig to use.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
StackManager instance.
|
|
451
|
+
"""
|
|
452
|
+
return StackManager.get_instance(config)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def get_active_stack() -> StackConfig | None:
|
|
456
|
+
"""Get the currently active stack configuration.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Active stack configuration or None.
|
|
460
|
+
"""
|
|
461
|
+
return get_stack_manager().active_stack
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def list_stacks() -> list[str]:
|
|
465
|
+
"""List all available stack names.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
List of stack names.
|
|
469
|
+
"""
|
|
470
|
+
return get_stack_manager().list_stacks()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def set_active_stack(name: str) -> bool:
|
|
474
|
+
"""Set the active stack by name.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
name: Stack name to activate.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if successful.
|
|
481
|
+
"""
|
|
482
|
+
return get_stack_manager().set_active_stack(name)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@contextmanager
|
|
486
|
+
def use_stack(name: str):
|
|
487
|
+
"""Context manager for temporarily using a different stack.
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
with use_stack("gcp-prod"):
|
|
491
|
+
pipeline.run()
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
name: Stack name to use.
|
|
495
|
+
|
|
496
|
+
Yields:
|
|
497
|
+
The stack configuration.
|
|
498
|
+
"""
|
|
499
|
+
with get_stack_manager().use_stack(name) as stack:
|
|
500
|
+
yield stack
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def use_stack_decorator(stack_name: str) -> Callable:
|
|
504
|
+
"""Decorator to run a function with a specific stack.
|
|
505
|
+
|
|
506
|
+
Example:
|
|
507
|
+
@use_stack_decorator("gcp-prod")
|
|
508
|
+
def train():
|
|
509
|
+
pipeline.run()
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
stack_name: Stack name to use.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Decorator function.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
def decorator(func: Callable) -> Callable:
|
|
519
|
+
def wrapper(*args, **kwargs):
|
|
520
|
+
with use_stack(stack_name):
|
|
521
|
+
return func(*args, **kwargs)
|
|
522
|
+
|
|
523
|
+
return wrapper
|
|
524
|
+
|
|
525
|
+
return decorator
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def get_routing_for_type(artifact_type: str) -> ArtifactRoutingRule | None:
|
|
529
|
+
"""Get artifact routing configuration for a type.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
artifact_type: Name of the artifact type (Model, Dataset, etc.)
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Routing rule or None.
|
|
536
|
+
"""
|
|
537
|
+
return get_stack_manager().get_routing_for_type(artifact_type)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""FlowyML Artifact Store Plugins."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from flowyml.plugins.stores.s3 import S3ArtifactStore
|
|
5
|
+
except ImportError:
|
|
6
|
+
S3ArtifactStore = None # boto3 not installed
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from flowyml.plugins.stores.gcs import GCSArtifactStore
|
|
10
|
+
except ImportError:
|
|
11
|
+
GCSArtifactStore = None # google-cloud-storage not installed
|
|
12
|
+
|
|
13
|
+
__all__ = ["S3ArtifactStore", "GCSArtifactStore"]
|