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,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"]
|