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.
- flowyml/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- 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 +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- 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 +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- 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/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- 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/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- 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/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -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.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""FlowyML Plugin Configuration System.
|
|
2
|
+
|
|
3
|
+
This module provides YAML-based configuration for plugins, allowing
|
|
4
|
+
users to define their stack configuration in a file and use plugins
|
|
5
|
+
without manual setup in code.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# flowyml.yaml
|
|
9
|
+
plugins:
|
|
10
|
+
experiment_tracker:
|
|
11
|
+
type: mlflow
|
|
12
|
+
tracking_uri: http://localhost:5000
|
|
13
|
+
experiment_name: my_experiments
|
|
14
|
+
|
|
15
|
+
artifact_store:
|
|
16
|
+
type: gcs
|
|
17
|
+
bucket: my-ml-artifacts
|
|
18
|
+
prefix: experiments/
|
|
19
|
+
project: my-gcp-project
|
|
20
|
+
|
|
21
|
+
orchestrator:
|
|
22
|
+
type: vertex_ai
|
|
23
|
+
project: my-gcp-project
|
|
24
|
+
location: us-central1
|
|
25
|
+
|
|
26
|
+
# In code - just use
|
|
27
|
+
from flowyml.plugins.config import get_tracker, get_artifact_store
|
|
28
|
+
|
|
29
|
+
tracker = get_tracker() # Uses config from flowyml.yaml
|
|
30
|
+
tracker.start_run("my_run")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import yaml
|
|
35
|
+
import logging
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from flowyml.plugins.manager import get_manager
|
|
40
|
+
from flowyml.plugins.base import (
|
|
41
|
+
BasePlugin,
|
|
42
|
+
ExperimentTracker,
|
|
43
|
+
ArtifactStorePlugin,
|
|
44
|
+
OrchestratorPlugin,
|
|
45
|
+
ContainerRegistryPlugin,
|
|
46
|
+
FeatureStorePlugin,
|
|
47
|
+
DataValidatorPlugin,
|
|
48
|
+
AlerterPlugin,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Default config file names to search for
|
|
55
|
+
CONFIG_FILE_NAMES = [
|
|
56
|
+
"flowyml.yaml",
|
|
57
|
+
"flowyml.yml",
|
|
58
|
+
".flowyml.yaml",
|
|
59
|
+
".flowyml.yml",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PluginConfig:
|
|
64
|
+
"""Manages plugin configuration from YAML files.
|
|
65
|
+
|
|
66
|
+
Configuration can be loaded from:
|
|
67
|
+
1. A specific file path
|
|
68
|
+
2. Auto-discovered from current directory
|
|
69
|
+
3. Environment variable FLOWYML_CONFIG
|
|
70
|
+
|
|
71
|
+
Example flowyml.yaml:
|
|
72
|
+
|
|
73
|
+
plugins:
|
|
74
|
+
experiment_tracker:
|
|
75
|
+
type: mlflow
|
|
76
|
+
tracking_uri: http://localhost:5000
|
|
77
|
+
experiment_name: my_experiments
|
|
78
|
+
|
|
79
|
+
artifact_store:
|
|
80
|
+
type: s3
|
|
81
|
+
bucket: my-ml-artifacts
|
|
82
|
+
prefix: experiments/
|
|
83
|
+
|
|
84
|
+
orchestrator:
|
|
85
|
+
type: kubernetes
|
|
86
|
+
namespace: ml-pipelines
|
|
87
|
+
|
|
88
|
+
container_registry:
|
|
89
|
+
type: gcr
|
|
90
|
+
project: my-gcp-project
|
|
91
|
+
location: us-central1
|
|
92
|
+
repository: ml-images
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, config_path: str = None):
|
|
96
|
+
"""Initialize the plugin configuration.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config_path: Optional path to config file. If not provided,
|
|
100
|
+
auto-discovers from current directory.
|
|
101
|
+
"""
|
|
102
|
+
self._config_path = config_path
|
|
103
|
+
self._config: dict = {}
|
|
104
|
+
self._manager = get_manager()
|
|
105
|
+
self._instances: dict[str, BasePlugin] = {}
|
|
106
|
+
|
|
107
|
+
# Load config
|
|
108
|
+
self._load_config()
|
|
109
|
+
|
|
110
|
+
def _load_config(self) -> None:
|
|
111
|
+
"""Load configuration from file."""
|
|
112
|
+
config_path = self._find_config_file()
|
|
113
|
+
|
|
114
|
+
if config_path:
|
|
115
|
+
try:
|
|
116
|
+
with open(config_path) as f:
|
|
117
|
+
raw_config = yaml.safe_load(f) or {}
|
|
118
|
+
|
|
119
|
+
# Substitute environment variables
|
|
120
|
+
self._config = self._substitute_env_vars(raw_config)
|
|
121
|
+
self._config_path = config_path
|
|
122
|
+
logger.info(f"Loaded plugin config from: {config_path}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.warning(f"Failed to load config from {config_path}: {e}")
|
|
125
|
+
self._config = {}
|
|
126
|
+
else:
|
|
127
|
+
logger.debug("No config file found, using defaults")
|
|
128
|
+
self._config = {}
|
|
129
|
+
|
|
130
|
+
def _substitute_env_vars(self, obj: Any) -> Any:
|
|
131
|
+
"""Recursively substitute environment variables in config.
|
|
132
|
+
|
|
133
|
+
Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
obj: Config object (dict, list, or scalar).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Object with environment variables substituted.
|
|
140
|
+
"""
|
|
141
|
+
import re
|
|
142
|
+
|
|
143
|
+
if isinstance(obj, dict):
|
|
144
|
+
return {k: self._substitute_env_vars(v) for k, v in obj.items()}
|
|
145
|
+
elif isinstance(obj, list):
|
|
146
|
+
return [self._substitute_env_vars(item) for item in obj]
|
|
147
|
+
elif isinstance(obj, str):
|
|
148
|
+
# Pattern: ${VAR_NAME} or ${VAR_NAME:-default}
|
|
149
|
+
pattern = r"\$\{([^}:]+)(?::-([^}]*))?\}"
|
|
150
|
+
|
|
151
|
+
def replace(match):
|
|
152
|
+
var_name = match.group(1)
|
|
153
|
+
default = match.group(2)
|
|
154
|
+
value = os.environ.get(var_name)
|
|
155
|
+
if value is not None:
|
|
156
|
+
return value
|
|
157
|
+
elif default is not None:
|
|
158
|
+
return default
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(f"Environment variable '{var_name}' not set")
|
|
161
|
+
return match.group(0) # Keep original if not found
|
|
162
|
+
|
|
163
|
+
return re.sub(pattern, replace, obj)
|
|
164
|
+
else:
|
|
165
|
+
return obj
|
|
166
|
+
|
|
167
|
+
def _find_config_file(self) -> str | None:
|
|
168
|
+
"""Find the configuration file.
|
|
169
|
+
|
|
170
|
+
Searches in order:
|
|
171
|
+
1. Explicit path provided to constructor
|
|
172
|
+
2. FLOWYML_CONFIG environment variable
|
|
173
|
+
3. Current directory and parent directories
|
|
174
|
+
"""
|
|
175
|
+
# Check explicit path
|
|
176
|
+
if self._config_path:
|
|
177
|
+
if os.path.exists(self._config_path):
|
|
178
|
+
return self._config_path
|
|
179
|
+
logger.warning(f"Config file not found: {self._config_path}")
|
|
180
|
+
|
|
181
|
+
# Check environment variable
|
|
182
|
+
env_config = os.environ.get("FLOWYML_CONFIG")
|
|
183
|
+
if env_config and os.path.exists(env_config):
|
|
184
|
+
return env_config
|
|
185
|
+
|
|
186
|
+
# Search current directory and parents
|
|
187
|
+
current = Path.cwd()
|
|
188
|
+
for _ in range(5): # Search up to 5 levels
|
|
189
|
+
for name in CONFIG_FILE_NAMES:
|
|
190
|
+
config_file = current / name
|
|
191
|
+
if config_file.exists():
|
|
192
|
+
return str(config_file)
|
|
193
|
+
parent = current.parent
|
|
194
|
+
if parent == current:
|
|
195
|
+
break
|
|
196
|
+
current = parent
|
|
197
|
+
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def reload(self) -> None:
|
|
201
|
+
"""Reload configuration from file."""
|
|
202
|
+
self._instances.clear()
|
|
203
|
+
self._load_config()
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def plugins_config(self) -> dict:
|
|
207
|
+
"""Get the plugins configuration section."""
|
|
208
|
+
return self._config.get("plugins", {})
|
|
209
|
+
|
|
210
|
+
def get_plugin_config(self, plugin_role: str) -> dict | None:
|
|
211
|
+
"""Get configuration for a specific plugin role.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
plugin_role: Role like 'experiment_tracker', 'artifact_store', etc.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Configuration dictionary or None.
|
|
218
|
+
"""
|
|
219
|
+
return self.plugins_config.get(plugin_role)
|
|
220
|
+
|
|
221
|
+
def _get_plugin(self, plugin_role: str, plugin_class: type = None) -> BasePlugin | None:
|
|
222
|
+
"""Get or create a plugin instance for a role.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
plugin_role: Role like 'experiment_tracker'.
|
|
226
|
+
plugin_class: Optional base class to validate against.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Plugin instance or None if not configured.
|
|
230
|
+
"""
|
|
231
|
+
# Check cache
|
|
232
|
+
if plugin_role in self._instances:
|
|
233
|
+
return self._instances[plugin_role]
|
|
234
|
+
|
|
235
|
+
# Get config
|
|
236
|
+
config = self.get_plugin_config(plugin_role)
|
|
237
|
+
if not config:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
# Extract plugin type
|
|
241
|
+
plugin_type = config.pop("type", None)
|
|
242
|
+
if not plugin_type:
|
|
243
|
+
logger.error(f"Plugin config for '{plugin_role}' missing 'type'")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
# Check if installed
|
|
247
|
+
if not self._manager.is_installed(plugin_type):
|
|
248
|
+
logger.info(f"Installing plugin '{plugin_type}'...")
|
|
249
|
+
self._manager.install(plugin_type)
|
|
250
|
+
|
|
251
|
+
# Load and instantiate
|
|
252
|
+
try:
|
|
253
|
+
plugin = self._manager.get_instance(plugin_type, **config)
|
|
254
|
+
plugin.initialize()
|
|
255
|
+
self._instances[plugin_role] = plugin
|
|
256
|
+
return plugin
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"Failed to create plugin '{plugin_type}': {e}")
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def get_experiment_tracker(self) -> ExperimentTracker | None:
|
|
262
|
+
"""Get the configured experiment tracker."""
|
|
263
|
+
return self._get_plugin("experiment_tracker", ExperimentTracker)
|
|
264
|
+
|
|
265
|
+
def get_artifact_store(self) -> ArtifactStorePlugin | None:
|
|
266
|
+
"""Get the configured artifact store."""
|
|
267
|
+
return self._get_plugin("artifact_store", ArtifactStorePlugin)
|
|
268
|
+
|
|
269
|
+
def get_orchestrator(self) -> OrchestratorPlugin | None:
|
|
270
|
+
"""Get the configured orchestrator."""
|
|
271
|
+
return self._get_plugin("orchestrator", OrchestratorPlugin)
|
|
272
|
+
|
|
273
|
+
def get_container_registry(self) -> ContainerRegistryPlugin | None:
|
|
274
|
+
"""Get the configured container registry."""
|
|
275
|
+
return self._get_plugin("container_registry", ContainerRegistryPlugin)
|
|
276
|
+
|
|
277
|
+
def get_feature_store(self) -> FeatureStorePlugin | None:
|
|
278
|
+
"""Get the configured feature store."""
|
|
279
|
+
return self._get_plugin("feature_store", FeatureStorePlugin)
|
|
280
|
+
|
|
281
|
+
def get_data_validator(self) -> DataValidatorPlugin | None:
|
|
282
|
+
"""Get the configured data validator."""
|
|
283
|
+
return self._get_plugin("data_validator", DataValidatorPlugin)
|
|
284
|
+
|
|
285
|
+
def get_alerter(self) -> AlerterPlugin | None:
|
|
286
|
+
"""Get the configured alerter."""
|
|
287
|
+
return self._get_plugin("alerter", AlerterPlugin)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# =============================================================================
|
|
291
|
+
# GLOBAL CONFIG INSTANCE
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
_config: PluginConfig | None = None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_config(config_path: str = None) -> PluginConfig:
|
|
298
|
+
"""Get the global plugin configuration.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
config_path: Optional path to config file.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
PluginConfig instance.
|
|
305
|
+
"""
|
|
306
|
+
global _config
|
|
307
|
+
if _config is None or config_path:
|
|
308
|
+
_config = PluginConfig(config_path)
|
|
309
|
+
return _config
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def reload_config() -> None:
|
|
313
|
+
"""Reload the global configuration from file."""
|
|
314
|
+
global _config
|
|
315
|
+
if _config:
|
|
316
|
+
_config.reload()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# =============================================================================
|
|
320
|
+
# CONVENIENCE FUNCTIONS
|
|
321
|
+
# =============================================================================
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_tracker() -> ExperimentTracker | None:
|
|
325
|
+
"""Get the configured experiment tracker.
|
|
326
|
+
|
|
327
|
+
Reads configuration from flowyml.yaml and returns the configured
|
|
328
|
+
experiment tracker, ready to use.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
tracker = get_tracker()
|
|
332
|
+
if tracker:
|
|
333
|
+
tracker.start_run("my_experiment")
|
|
334
|
+
"""
|
|
335
|
+
return get_config().get_experiment_tracker()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_artifact_store() -> ArtifactStorePlugin | None:
|
|
339
|
+
"""Get the configured artifact store.
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
store = get_artifact_store()
|
|
343
|
+
if store:
|
|
344
|
+
store.save(model, "models/latest.pkl")
|
|
345
|
+
"""
|
|
346
|
+
return get_config().get_artifact_store()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_orchestrator() -> OrchestratorPlugin | None:
|
|
350
|
+
"""Get the configured orchestrator.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
orchestrator = get_orchestrator()
|
|
354
|
+
if orchestrator:
|
|
355
|
+
orchestrator.run_pipeline(my_pipeline, "run-001")
|
|
356
|
+
"""
|
|
357
|
+
return get_config().get_orchestrator()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_container_registry() -> ContainerRegistryPlugin | None:
|
|
361
|
+
"""Get the configured container registry."""
|
|
362
|
+
return get_config().get_container_registry()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_feature_store() -> FeatureStorePlugin | None:
|
|
366
|
+
"""Get the configured feature store."""
|
|
367
|
+
return get_config().get_feature_store()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def get_data_validator() -> DataValidatorPlugin | None:
|
|
371
|
+
"""Get the configured data validator."""
|
|
372
|
+
return get_config().get_data_validator()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_alerter() -> AlerterPlugin | None:
|
|
376
|
+
"""Get the configured alerter."""
|
|
377
|
+
return get_config().get_alerter()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# =============================================================================
|
|
381
|
+
# CLI INIT COMMAND SUPPORT
|
|
382
|
+
# =============================================================================
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def generate_config_template(
|
|
386
|
+
tracker: str = None,
|
|
387
|
+
store: str = None,
|
|
388
|
+
orchestrator: str = None,
|
|
389
|
+
registry: str = None,
|
|
390
|
+
) -> str:
|
|
391
|
+
"""Generate a flowyml.yaml template.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
tracker: Experiment tracker plugin name.
|
|
395
|
+
store: Artifact store plugin name.
|
|
396
|
+
orchestrator: Orchestrator plugin name.
|
|
397
|
+
registry: Container registry plugin name.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
YAML configuration string.
|
|
401
|
+
"""
|
|
402
|
+
config = {
|
|
403
|
+
"# FlowyML Configuration": None,
|
|
404
|
+
"# Run 'flowyml plugin list' to see available plugins": None,
|
|
405
|
+
"plugins": {},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if tracker:
|
|
409
|
+
config["plugins"]["experiment_tracker"] = {
|
|
410
|
+
"type": tracker,
|
|
411
|
+
"# Add plugin-specific configuration below": None,
|
|
412
|
+
}
|
|
413
|
+
if tracker == "mlflow":
|
|
414
|
+
config["plugins"]["experiment_tracker"].update(
|
|
415
|
+
{
|
|
416
|
+
"tracking_uri": "http://localhost:5000",
|
|
417
|
+
"experiment_name": "my_experiments",
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if store:
|
|
422
|
+
config["plugins"]["artifact_store"] = {
|
|
423
|
+
"type": store,
|
|
424
|
+
}
|
|
425
|
+
if store == "s3":
|
|
426
|
+
config["plugins"]["artifact_store"].update(
|
|
427
|
+
{
|
|
428
|
+
"bucket": "my-ml-artifacts",
|
|
429
|
+
"prefix": "experiments/",
|
|
430
|
+
},
|
|
431
|
+
)
|
|
432
|
+
elif store == "gcs":
|
|
433
|
+
config["plugins"]["artifact_store"].update(
|
|
434
|
+
{
|
|
435
|
+
"bucket": "my-ml-artifacts",
|
|
436
|
+
"prefix": "experiments/",
|
|
437
|
+
"project": "my-gcp-project",
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if orchestrator:
|
|
442
|
+
config["plugins"]["orchestrator"] = {
|
|
443
|
+
"type": orchestrator,
|
|
444
|
+
}
|
|
445
|
+
if orchestrator == "vertex_ai":
|
|
446
|
+
config["plugins"]["orchestrator"].update(
|
|
447
|
+
{
|
|
448
|
+
"project": "my-gcp-project",
|
|
449
|
+
"location": "us-central1",
|
|
450
|
+
"staging_bucket": "gs://my-staging-bucket",
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
elif orchestrator == "kubernetes":
|
|
454
|
+
config["plugins"]["orchestrator"].update(
|
|
455
|
+
{
|
|
456
|
+
"namespace": "ml-pipelines",
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if registry:
|
|
461
|
+
config["plugins"]["container_registry"] = {
|
|
462
|
+
"type": registry,
|
|
463
|
+
}
|
|
464
|
+
if registry == "gcr":
|
|
465
|
+
config["plugins"]["container_registry"].update(
|
|
466
|
+
{
|
|
467
|
+
"project": "my-gcp-project",
|
|
468
|
+
"location": "us-central1",
|
|
469
|
+
"repository": "ml-images",
|
|
470
|
+
"use_artifact_registry": True,
|
|
471
|
+
},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Generate YAML
|
|
475
|
+
lines = ["# FlowyML Configuration", "# Run 'flowyml plugin list' to see available plugins", ""]
|
|
476
|
+
lines.append(yaml.dump({"plugins": config["plugins"]}, default_flow_style=False, sort_keys=False))
|
|
477
|
+
|
|
478
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""FlowyML Model Deployer Plugins - Native implementations for model deployment.
|
|
2
|
+
|
|
3
|
+
This module provides model deployer plugin implementations for:
|
|
4
|
+
- Vertex AI Endpoints (GCP)
|
|
5
|
+
- SageMaker Endpoints (AWS)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from flowyml.plugins.deployers import VertexEndpointDeployer, SageMakerEndpointDeployer
|
|
9
|
+
|
|
10
|
+
# Or via config
|
|
11
|
+
# model_deployer:
|
|
12
|
+
# type: vertex_endpoint
|
|
13
|
+
# project: my-gcp-project
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from flowyml.plugins.deployers.vertex import VertexEndpointDeployer
|
|
17
|
+
from flowyml.plugins.deployers.sagemaker import SageMakerEndpointDeployer
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"VertexEndpointDeployer",
|
|
21
|
+
"SageMakerEndpointDeployer",
|
|
22
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""GCP Cloud Run Deployer - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This plugin provides direct integration with Google Cloud Run
|
|
4
|
+
for serverless model serving.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
from flowyml.utils.observability import trace_execution
|
|
12
|
+
|
|
13
|
+
from flowyml.plugins.base import ModelDeployerPlugin, PluginMetadata, PluginType
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# import removed from here
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GCPCloudRunDeployer(ModelDeployerPlugin):
|
|
22
|
+
"""Native GCP Cloud Run deployer for FlowyML.
|
|
23
|
+
|
|
24
|
+
This plugin deploys containerized models to Cloud Run.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_id: GCP project ID.
|
|
28
|
+
region: GCP region (default: us-central1).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
metadata = PluginMetadata(
|
|
32
|
+
name="gcp_cloud_run",
|
|
33
|
+
version="1.0.0",
|
|
34
|
+
description="Google Cloud Run Deployer",
|
|
35
|
+
author="FlowyML Team",
|
|
36
|
+
plugin_type=PluginType.MODEL_DEPLOYER,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
project_id: str,
|
|
42
|
+
region: str = "us-central1",
|
|
43
|
+
**kwargs,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(**kwargs)
|
|
46
|
+
self.project_id = project_id
|
|
47
|
+
self.region = region
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def plugin_type(self) -> PluginType:
|
|
51
|
+
return PluginType.MODEL_DEPLOYER
|
|
52
|
+
|
|
53
|
+
def initialize(self) -> None:
|
|
54
|
+
"""Verify gcloud is installed."""
|
|
55
|
+
try:
|
|
56
|
+
subprocess.run(["gcloud", "--version"], check=True, capture_output=True)
|
|
57
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"gcloud CLI is required for GCP Cloud Run deployment. " "Please install the Google Cloud SDK.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@trace_execution(operation_name="cloud_run_deploy")
|
|
63
|
+
def deploy(
|
|
64
|
+
self,
|
|
65
|
+
model_uri: str,
|
|
66
|
+
endpoint_name: str,
|
|
67
|
+
image: str = None,
|
|
68
|
+
memory: str = "512Mi",
|
|
69
|
+
cpu: str = "1",
|
|
70
|
+
min_instances: int = 0,
|
|
71
|
+
max_instances: int = 10,
|
|
72
|
+
allow_unauthenticated: bool = True,
|
|
73
|
+
env_vars: dict[str, str] = None,
|
|
74
|
+
**kwargs,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Deploy a container to Cloud Run.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
model_uri: Not used directly for Cloud Run (uses image), but kept for interface compatibility.
|
|
80
|
+
endpoint_name: Name of the Cloud Run service.
|
|
81
|
+
image: Docker image to deploy (required).
|
|
82
|
+
memory: Memory limit (e.g., 512Mi, 2Gi).
|
|
83
|
+
cpu: CPU limit.
|
|
84
|
+
min_instances: Minimum number of instances.
|
|
85
|
+
max_instances: Maximum number of instances.
|
|
86
|
+
allow_unauthenticated: Allow public access.
|
|
87
|
+
env_vars: Environment variables.
|
|
88
|
+
**kwargs: Additional arguments.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Service URL.
|
|
92
|
+
"""
|
|
93
|
+
if not image:
|
|
94
|
+
raise ValueError("Docker image is required for Cloud Run deployment.")
|
|
95
|
+
|
|
96
|
+
command = [
|
|
97
|
+
"gcloud",
|
|
98
|
+
"run",
|
|
99
|
+
"deploy",
|
|
100
|
+
endpoint_name,
|
|
101
|
+
f"--image={image}",
|
|
102
|
+
f"--region={self.region}",
|
|
103
|
+
f"--project={self.project_id}",
|
|
104
|
+
f"--memory={memory}",
|
|
105
|
+
f"--cpu={cpu}",
|
|
106
|
+
f"--min-instances={min_instances}",
|
|
107
|
+
f"--max-instances={max_instances}",
|
|
108
|
+
"--platform=managed",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
if allow_unauthenticated:
|
|
112
|
+
command.append("--allow-unauthenticated")
|
|
113
|
+
else:
|
|
114
|
+
command.append("--no-allow-unauthenticated")
|
|
115
|
+
|
|
116
|
+
if env_vars:
|
|
117
|
+
env_list = [f"{k}={v}" for k, v in env_vars.items()]
|
|
118
|
+
command.append(f"--set-env-vars={','.join(env_list)}")
|
|
119
|
+
|
|
120
|
+
if model_uri:
|
|
121
|
+
# Pass model URI as environment variable if provided
|
|
122
|
+
command.append(f"--set-env-vars=MODEL_URI={model_uri}")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
logger.info(f"Deploying Cloud Run service: {endpoint_name}...")
|
|
126
|
+
subprocess.run(command, check=True)
|
|
127
|
+
|
|
128
|
+
# Get service URL
|
|
129
|
+
url_cmd = [
|
|
130
|
+
"gcloud",
|
|
131
|
+
"run",
|
|
132
|
+
"services",
|
|
133
|
+
"describe",
|
|
134
|
+
endpoint_name,
|
|
135
|
+
f"--region={self.region}",
|
|
136
|
+
f"--project={self.project_id}",
|
|
137
|
+
"--format=value(status.url)",
|
|
138
|
+
]
|
|
139
|
+
result = subprocess.run(url_cmd, check=True, capture_output=True, text=True)
|
|
140
|
+
url = result.stdout.strip()
|
|
141
|
+
|
|
142
|
+
logger.info(f"Deployed successfully to: {url}")
|
|
143
|
+
return url
|
|
144
|
+
|
|
145
|
+
except subprocess.CalledProcessError as e:
|
|
146
|
+
logger.error(f"Failed to deploy to Cloud Run: {e}")
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
def get_endpoint(self, endpoint_name: str) -> dict | None:
|
|
150
|
+
"""Get Cloud Run service details."""
|
|
151
|
+
try:
|
|
152
|
+
cmd = [
|
|
153
|
+
"gcloud",
|
|
154
|
+
"run",
|
|
155
|
+
"services",
|
|
156
|
+
"describe",
|
|
157
|
+
endpoint_name,
|
|
158
|
+
f"--region={self.region}",
|
|
159
|
+
f"--project={self.project_id}",
|
|
160
|
+
"--format=json",
|
|
161
|
+
]
|
|
162
|
+
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
163
|
+
return json.loads(result.stdout)
|
|
164
|
+
except subprocess.CalledProcessError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
@trace_execution(operation_name="cloud_run_undeploy")
|
|
168
|
+
def undeploy(self, endpoint_name: str) -> bool:
|
|
169
|
+
"""Delete Cloud Run service."""
|
|
170
|
+
try:
|
|
171
|
+
cmd = [
|
|
172
|
+
"gcloud",
|
|
173
|
+
"run",
|
|
174
|
+
"services",
|
|
175
|
+
"delete",
|
|
176
|
+
endpoint_name,
|
|
177
|
+
f"--region={self.region}",
|
|
178
|
+
f"--project={self.project_id}",
|
|
179
|
+
"--quiet",
|
|
180
|
+
]
|
|
181
|
+
subprocess.run(cmd, check=True)
|
|
182
|
+
logger.info(f"Deleted Cloud Run service: {endpoint_name}")
|
|
183
|
+
return True
|
|
184
|
+
except subprocess.CalledProcessError as e:
|
|
185
|
+
logger.error(f"Failed to delete service: {e}")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
@trace_execution(operation_name="cloud_run_predict")
|
|
189
|
+
def predict(self, endpoint: str, data: Any) -> Any:
|
|
190
|
+
"""Make prediction (helper using curl/requests if desired, but usually done via HTTP client)."""
|
|
191
|
+
# Simple implementation using requests if available, or just printing instructions
|
|
192
|
+
import requests
|
|
193
|
+
|
|
194
|
+
if isinstance(data, (dict, list)):
|
|
195
|
+
resp = requests.post(endpoint, json=data)
|
|
196
|
+
else:
|
|
197
|
+
resp = requests.post(endpoint, data=data)
|
|
198
|
+
|
|
199
|
+
resp.raise_for_status()
|
|
200
|
+
return resp.json()
|