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,453 @@
1
+ """S3 Artifact Store - Native FlowyML Plugin.
2
+
3
+ This is a native FlowyML implementation for AWS S3 artifact storage,
4
+ without requiring any external framework dependencies.
5
+
6
+ Usage:
7
+ from flowyml.plugins import get_plugin
8
+
9
+ store = get_plugin("s3",
10
+ bucket="my-ml-artifacts",
11
+ prefix="experiments/"
12
+ )
13
+
14
+ # Save artifacts
15
+ store.save(my_model, "models/model.pkl")
16
+
17
+ # Load artifacts
18
+ model = store.load("models/model.pkl")
19
+ """
20
+
21
+ import logging
22
+ from typing import Any
23
+ import pickle
24
+ import json
25
+
26
+ from flowyml.plugins.base import ArtifactStorePlugin, PluginMetadata, PluginType
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class S3ArtifactStore(ArtifactStorePlugin):
32
+ """Native S3 artifact store for FlowyML.
33
+
34
+ This store integrates directly with AWS S3 without any
35
+ intermediate framework, providing full control over
36
+ artifact storage.
37
+
38
+ Args:
39
+ bucket: S3 bucket name.
40
+ prefix: Optional prefix/folder within the bucket.
41
+ region: AWS region (uses default if not provided).
42
+ access_key: AWS access key (uses environment/credentials if not provided).
43
+ secret_key: AWS secret key (uses environment/credentials if not provided).
44
+ endpoint_url: Custom S3 endpoint (for S3-compatible services like MinIO).
45
+
46
+ Example:
47
+ store = S3ArtifactStore(
48
+ bucket="my-ml-artifacts",
49
+ prefix="experiments/",
50
+ region="us-east-1"
51
+ )
52
+
53
+ # Save a model
54
+ store.save(trained_model, "models/classifier.pkl")
55
+
56
+ # Load a model
57
+ model = store.load("models/classifier.pkl")
58
+
59
+ # Check if exists
60
+ if store.exists("models/classifier.pkl"):
61
+ print("Model found!")
62
+ """
63
+
64
+ METADATA = PluginMetadata(
65
+ name="s3",
66
+ description="AWS S3 artifact storage",
67
+ plugin_type=PluginType.ARTIFACT_STORE,
68
+ version="1.0.0",
69
+ author="FlowyML",
70
+ packages=["boto3>=1.28", "s3fs>=2023.0"],
71
+ documentation_url="https://docs.aws.amazon.com/s3/",
72
+ tags=["artifact-store", "aws", "cloud", "popular"],
73
+ )
74
+
75
+ def __init__(
76
+ self,
77
+ bucket: str,
78
+ prefix: str = "",
79
+ region: str = None,
80
+ access_key: str = None,
81
+ secret_key: str = None,
82
+ endpoint_url: str = None,
83
+ **kwargs,
84
+ ):
85
+ """Initialize the S3 artifact store."""
86
+ super().__init__(
87
+ name=kwargs.pop("name", "s3"),
88
+ bucket=bucket,
89
+ prefix=prefix,
90
+ region=region,
91
+ access_key=access_key,
92
+ secret_key=secret_key,
93
+ endpoint_url=endpoint_url,
94
+ **kwargs,
95
+ )
96
+
97
+ self._s3_client = None
98
+ self._s3fs = None
99
+ self._bucket = bucket
100
+ self._prefix = prefix.strip("/")
101
+
102
+ def initialize(self) -> None:
103
+ """Initialize S3 connection."""
104
+ try:
105
+ import boto3
106
+
107
+ # Build client kwargs
108
+ client_kwargs = {}
109
+
110
+ if self._config.get("region"):
111
+ client_kwargs["region_name"] = self._config["region"]
112
+
113
+ if self._config.get("endpoint_url"):
114
+ client_kwargs["endpoint_url"] = self._config["endpoint_url"]
115
+
116
+ if self._config.get("access_key") and self._config.get("secret_key"):
117
+ client_kwargs["aws_access_key_id"] = self._config["access_key"]
118
+ client_kwargs["aws_secret_access_key"] = self._config["secret_key"]
119
+
120
+ self._s3_client = boto3.client("s3", **client_kwargs)
121
+
122
+ # Optionally initialize s3fs for filesystem-like operations
123
+ try:
124
+ import s3fs
125
+
126
+ fs_kwargs = {}
127
+ if self._config.get("endpoint_url"):
128
+ fs_kwargs["client_kwargs"] = {"endpoint_url": self._config["endpoint_url"]}
129
+ if self._config.get("access_key"):
130
+ fs_kwargs["key"] = self._config["access_key"]
131
+ fs_kwargs["secret"] = self._config["secret_key"]
132
+ self._s3fs = s3fs.S3FileSystem(**fs_kwargs)
133
+ except ImportError:
134
+ logger.debug("s3fs not available, using boto3 only")
135
+
136
+ self._is_initialized = True
137
+ logger.info(f"S3 artifact store initialized: s3://{self._bucket}/{self._prefix}")
138
+
139
+ except ImportError:
140
+ raise ImportError(
141
+ "boto3 is not installed. Run: flowyml plugin install s3",
142
+ )
143
+
144
+ def _ensure_initialized(self) -> None:
145
+ """Ensure S3 is initialized."""
146
+ if not self._is_initialized:
147
+ self.initialize()
148
+
149
+ def _get_full_path(self, path: str) -> str:
150
+ """Get the full S3 key for a path."""
151
+ if self._prefix:
152
+ return f"{self._prefix}/{path.lstrip('/')}"
153
+ return path.lstrip("/")
154
+
155
+ def _get_s3_uri(self, path: str) -> str:
156
+ """Get the full S3 URI for a path."""
157
+ key = self._get_full_path(path)
158
+ return f"s3://{self._bucket}/{key}"
159
+
160
+ def save(self, artifact: Any, path: str) -> str:
161
+ """Save an artifact to S3.
162
+
163
+ Args:
164
+ artifact: The artifact to save. Can be:
165
+ - bytes: Saved directly
166
+ - str: Saved as UTF-8 text
167
+ - dict/list: Saved as JSON
168
+ - Other objects: Pickled
169
+ path: Path within the store.
170
+
171
+ Returns:
172
+ Full S3 URI of the saved artifact.
173
+ """
174
+ self._ensure_initialized()
175
+
176
+ key = self._get_full_path(path)
177
+
178
+ # Determine how to serialize
179
+ if isinstance(artifact, bytes):
180
+ body = artifact
181
+ elif isinstance(artifact, str):
182
+ body = artifact.encode("utf-8")
183
+ elif isinstance(artifact, (dict, list)):
184
+ body = json.dumps(artifact).encode("utf-8")
185
+ else:
186
+ # Pickle the object
187
+ body = pickle.dumps(artifact)
188
+
189
+ self._s3_client.put_object(
190
+ Bucket=self._bucket,
191
+ Key=key,
192
+ Body=body,
193
+ )
194
+
195
+ uri = self._get_s3_uri(path)
196
+ logger.info(f"Saved artifact to {uri}")
197
+ return uri
198
+
199
+ def save_file(self, local_path: str, remote_path: str) -> str:
200
+ """Upload a local file to S3.
201
+
202
+ Args:
203
+ local_path: Path to local file.
204
+ remote_path: Path in S3.
205
+
206
+ Returns:
207
+ Full S3 URI.
208
+ """
209
+ self._ensure_initialized()
210
+
211
+ key = self._get_full_path(remote_path)
212
+ self._s3_client.upload_file(local_path, self._bucket, key)
213
+
214
+ uri = self._get_s3_uri(remote_path)
215
+ logger.info(f"Uploaded {local_path} to {uri}")
216
+ return uri
217
+
218
+ def load(self, path: str, deserialize: bool = True) -> Any:
219
+ """Load an artifact from S3.
220
+
221
+ Args:
222
+ path: Path to the artifact.
223
+ deserialize: If True, attempts to deserialize (JSON/pickle).
224
+ If False, returns raw bytes.
225
+
226
+ Returns:
227
+ The loaded artifact.
228
+ """
229
+ self._ensure_initialized()
230
+
231
+ key = self._get_full_path(path)
232
+
233
+ response = self._s3_client.get_object(Bucket=self._bucket, Key=key)
234
+ body = response["Body"].read()
235
+
236
+ if not deserialize:
237
+ return body
238
+
239
+ # Try to deserialize
240
+ # First try JSON
241
+ try:
242
+ return json.loads(body.decode("utf-8"))
243
+ except (json.JSONDecodeError, UnicodeDecodeError):
244
+ pass
245
+
246
+ # Try pickle
247
+ try:
248
+ return pickle.loads(body)
249
+ except Exception:
250
+ pass
251
+
252
+ # Try UTF-8 string
253
+ try:
254
+ return body.decode("utf-8")
255
+ except UnicodeDecodeError:
256
+ pass
257
+
258
+ # Return raw bytes
259
+ return body
260
+
261
+ def download_file(self, remote_path: str, local_path: str) -> str:
262
+ """Download a file from S3 to local filesystem.
263
+
264
+ Args:
265
+ remote_path: Path in S3.
266
+ local_path: Local destination path.
267
+
268
+ Returns:
269
+ Local path.
270
+ """
271
+ self._ensure_initialized()
272
+
273
+ key = self._get_full_path(remote_path)
274
+
275
+ # Ensure local directory exists
276
+ from pathlib import Path
277
+
278
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
279
+
280
+ self._s3_client.download_file(self._bucket, key, local_path)
281
+ logger.info(f"Downloaded {self._get_s3_uri(remote_path)} to {local_path}")
282
+ return local_path
283
+
284
+ def exists(self, path: str) -> bool:
285
+ """Check if an artifact exists in S3.
286
+
287
+ Args:
288
+ path: Path to check.
289
+
290
+ Returns:
291
+ True if the artifact exists.
292
+ """
293
+ self._ensure_initialized()
294
+
295
+ key = self._get_full_path(path)
296
+
297
+ try:
298
+ self._s3_client.head_object(Bucket=self._bucket, Key=key)
299
+ return True
300
+ except Exception:
301
+ return False
302
+
303
+ def delete(self, path: str) -> bool:
304
+ """Delete an artifact from S3.
305
+
306
+ Args:
307
+ path: Path to delete.
308
+
309
+ Returns:
310
+ True if deletion was successful.
311
+ """
312
+ self._ensure_initialized()
313
+
314
+ key = self._get_full_path(path)
315
+
316
+ try:
317
+ self._s3_client.delete_object(Bucket=self._bucket, Key=key)
318
+ logger.info(f"Deleted {self._get_s3_uri(path)}")
319
+ return True
320
+ except Exception as e:
321
+ logger.error(f"Failed to delete {path}: {e}")
322
+ return False
323
+
324
+ def list(self, path: str = "") -> list[str]: # noqa: A003
325
+ """List artifacts in an S3 directory.
326
+
327
+ Args:
328
+ path: Directory path to list.
329
+
330
+ Returns:
331
+ List of artifact paths (relative to prefix).
332
+ """
333
+ self._ensure_initialized()
334
+
335
+ prefix = self._get_full_path(path)
336
+ if prefix and not prefix.endswith("/"):
337
+ prefix += "/"
338
+
339
+ try:
340
+ response = self._s3_client.list_objects_v2(
341
+ Bucket=self._bucket,
342
+ Prefix=prefix,
343
+ )
344
+
345
+ items = []
346
+ for obj in response.get("Contents", []):
347
+ # Remove the base prefix to get relative path
348
+ key = obj["Key"]
349
+ if self._prefix:
350
+ key = key[len(self._prefix) + 1 :]
351
+ items.append(key)
352
+
353
+ return items
354
+
355
+ except Exception as e:
356
+ logger.error(f"Failed to list {path}: {e}")
357
+ return []
358
+
359
+ @property
360
+ def root_path(self) -> str:
361
+ """Get the root S3 URI."""
362
+ if self._prefix:
363
+ return f"s3://{self._bucket}/{self._prefix}"
364
+ return f"s3://{self._bucket}"
365
+
366
+ def get_uri(self, path: str) -> str:
367
+ """Get the full S3 URI for a path."""
368
+ return self._get_s3_uri(path)
369
+
370
+ def save_typed_artifact(
371
+ self,
372
+ artifact: Any,
373
+ path: str,
374
+ run_id: str = "",
375
+ step_name: str = "",
376
+ ) -> str:
377
+ """Save a FlowyML typed artifact with proper handling.
378
+
379
+ Handles Model, Dataset, Metrics, and Parameters types with
380
+ appropriate serialization and metadata.
381
+
382
+ Args:
383
+ artifact: A FlowyML artifact type (Model, Dataset, etc.)
384
+ path: Base path (will be formatted with run_id/step_name)
385
+ run_id: Pipeline run ID for path templating
386
+ step_name: Step name for path templating
387
+
388
+ Returns:
389
+ Full S3 URI of the saved artifact.
390
+ """
391
+ self._ensure_initialized()
392
+
393
+ # Detect artifact type
394
+ artifact_type = type(artifact).__name__
395
+
396
+ # Format path with run info
397
+ formatted_path = path.format(
398
+ run_id=run_id,
399
+ step_name=step_name,
400
+ artifact_name=artifact_type.lower(),
401
+ )
402
+
403
+ # Handle different artifact types
404
+ if artifact_type == "Model":
405
+ # Save model data
406
+ model_data = artifact.data if hasattr(artifact, "data") else artifact
407
+ model_path = f"{formatted_path}/model.pkl"
408
+ self.save(model_data, model_path)
409
+
410
+ # Save metadata
411
+ if hasattr(artifact, "metadata") and artifact.metadata:
412
+ self.save(artifact.metadata, f"{formatted_path}/metadata.json")
413
+
414
+ return self._get_s3_uri(formatted_path)
415
+
416
+ elif artifact_type == "Dataset":
417
+ # Save dataset
418
+ data = artifact.data if hasattr(artifact, "data") else artifact
419
+
420
+ # Check format
421
+ fmt = getattr(artifact, "format", "pickle")
422
+ if fmt == "parquet":
423
+ # Use parquet if available
424
+ try:
425
+ import pandas as pd
426
+ import io
427
+
428
+ if isinstance(data, pd.DataFrame):
429
+ buffer = io.BytesIO()
430
+ data.to_parquet(buffer)
431
+ buffer.seek(0)
432
+ key = self._get_full_path(f"{formatted_path}/data.parquet")
433
+ self._s3_client.put_object(
434
+ Bucket=self._bucket,
435
+ Key=key,
436
+ Body=buffer.getvalue(),
437
+ )
438
+ return self._get_s3_uri(formatted_path)
439
+ except ImportError:
440
+ pass
441
+
442
+ # Fallback to pickle
443
+ self.save(data, f"{formatted_path}/data.pkl")
444
+ return self._get_s3_uri(formatted_path)
445
+
446
+ elif artifact_type in ("Metrics", "Parameters"):
447
+ # Save as JSON
448
+ data = dict(artifact) if hasattr(artifact, "__iter__") else artifact
449
+ return self.save(data, f"{formatted_path}.json")
450
+
451
+ else:
452
+ # Generic artifact
453
+ return self.save(artifact, formatted_path)
@@ -0,0 +1,11 @@
1
+ """FlowyML Trackers Plugins."""
2
+
3
+ # Import trackers as they are implemented
4
+ # This allows: from flowyml.plugins.trackers import MLflowTracker
5
+
6
+ try:
7
+ from flowyml.plugins.trackers.mlflow import MLflowTracker
8
+ except ImportError:
9
+ MLflowTracker = None # MLflow not installed
10
+
11
+ __all__ = ["MLflowTracker"]