flowyml 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl

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