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,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()