flowyml 1.1.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/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Pandas materializer for DataFrame serialization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
PANDAS_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
PANDAS_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if PANDAS_AVAILABLE:
|
|
18
|
+
|
|
19
|
+
class PandasMaterializer(BaseMaterializer):
|
|
20
|
+
"""Materializer for Pandas DataFrames and Series."""
|
|
21
|
+
|
|
22
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
23
|
+
"""Save Pandas object to path.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
obj: Pandas DataFrame or Series
|
|
27
|
+
path: Directory path where object should be saved
|
|
28
|
+
"""
|
|
29
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
if isinstance(obj, pd.DataFrame):
|
|
32
|
+
# Save DataFrame as parquet (efficient) or CSV (fallback)
|
|
33
|
+
try:
|
|
34
|
+
data_path = path / "data.parquet"
|
|
35
|
+
obj.to_parquet(data_path, index=True)
|
|
36
|
+
format_used = "parquet"
|
|
37
|
+
except Exception:
|
|
38
|
+
data_path = path / "data.csv"
|
|
39
|
+
obj.to_csv(data_path, index=True)
|
|
40
|
+
format_used = "csv"
|
|
41
|
+
|
|
42
|
+
# Save metadata
|
|
43
|
+
metadata = {
|
|
44
|
+
"type": "pandas_dataframe",
|
|
45
|
+
"format": format_used,
|
|
46
|
+
"shape": list(obj.shape),
|
|
47
|
+
"columns": obj.columns.tolist(),
|
|
48
|
+
"dtypes": {col: str(dtype) for col, dtype in obj.dtypes.items()},
|
|
49
|
+
"index_name": obj.index.name,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
with open(path / "metadata.json", "w") as f:
|
|
53
|
+
json.dump(metadata, f, indent=2)
|
|
54
|
+
|
|
55
|
+
elif isinstance(obj, pd.Series):
|
|
56
|
+
# Save Series
|
|
57
|
+
try:
|
|
58
|
+
data_path = path / "data.parquet"
|
|
59
|
+
obj.to_frame().to_parquet(data_path, index=True)
|
|
60
|
+
format_used = "parquet"
|
|
61
|
+
except Exception:
|
|
62
|
+
data_path = path / "data.csv"
|
|
63
|
+
obj.to_csv(data_path, index=True, header=True)
|
|
64
|
+
format_used = "csv"
|
|
65
|
+
|
|
66
|
+
metadata = {
|
|
67
|
+
"type": "pandas_series",
|
|
68
|
+
"format": format_used,
|
|
69
|
+
"shape": len(obj),
|
|
70
|
+
"name": obj.name,
|
|
71
|
+
"dtype": str(obj.dtype),
|
|
72
|
+
"index_name": obj.index.name,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
with open(path / "metadata.json", "w") as f:
|
|
76
|
+
json.dump(metadata, f, indent=2)
|
|
77
|
+
|
|
78
|
+
def load(self, path: Path) -> Any:
|
|
79
|
+
"""Load Pandas object from path.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Directory path from which to load object
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Loaded Pandas DataFrame or Series
|
|
86
|
+
"""
|
|
87
|
+
# Load metadata
|
|
88
|
+
metadata_path = path / "metadata.json"
|
|
89
|
+
if metadata_path.exists():
|
|
90
|
+
with open(metadata_path) as f:
|
|
91
|
+
metadata = json.load(f)
|
|
92
|
+
else:
|
|
93
|
+
metadata = {}
|
|
94
|
+
|
|
95
|
+
obj_type = metadata.get("type", "pandas_dataframe")
|
|
96
|
+
format_used = metadata.get("format", "parquet")
|
|
97
|
+
|
|
98
|
+
if obj_type == "pandas_dataframe":
|
|
99
|
+
if format_used == "parquet":
|
|
100
|
+
data_path = path / "data.parquet"
|
|
101
|
+
return pd.read_parquet(data_path)
|
|
102
|
+
else:
|
|
103
|
+
data_path = path / "data.csv"
|
|
104
|
+
return pd.read_csv(data_path, index_col=0)
|
|
105
|
+
|
|
106
|
+
elif obj_type == "pandas_series":
|
|
107
|
+
if format_used == "parquet":
|
|
108
|
+
data_path = path / "data.parquet"
|
|
109
|
+
df = pd.read_parquet(data_path)
|
|
110
|
+
return df.iloc[:, 0]
|
|
111
|
+
else:
|
|
112
|
+
data_path = path / "data.csv"
|
|
113
|
+
series = pd.read_csv(data_path, index_col=0)
|
|
114
|
+
if isinstance(series, pd.DataFrame):
|
|
115
|
+
series = series.iloc[:, 0]
|
|
116
|
+
return series
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Unknown Pandas object type: {obj_type}")
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def supported_types(cls) -> list[type]:
|
|
123
|
+
"""Return Pandas types supported by this materializer."""
|
|
124
|
+
return [pd.DataFrame, pd.Series]
|
|
125
|
+
|
|
126
|
+
# Auto-register
|
|
127
|
+
register_materializer(PandasMaterializer)
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# Placeholder when Pandas not available
|
|
131
|
+
class PandasMaterializer(BaseMaterializer):
|
|
132
|
+
"""Placeholder materializer when Pandas is not installed."""
|
|
133
|
+
|
|
134
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
135
|
+
raise ImportError("Pandas is not installed. Install with: pip install pandas")
|
|
136
|
+
|
|
137
|
+
def load(self, path: Path) -> Any:
|
|
138
|
+
raise ImportError("Pandas is not installed. Install with: pip install pandas")
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def supported_types(cls) -> list[type]:
|
|
142
|
+
return []
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""PyTorch materializer for model serialization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
|
|
8
|
+
import contextlib
|
|
9
|
+
import builtins
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn as nn
|
|
14
|
+
|
|
15
|
+
# Verify PyTorch has expected attributes
|
|
16
|
+
_ = nn.Module
|
|
17
|
+
_ = torch.Tensor
|
|
18
|
+
PYTORCH_AVAILABLE = True
|
|
19
|
+
except (ImportError, AttributeError):
|
|
20
|
+
PYTORCH_AVAILABLE = False
|
|
21
|
+
torch = None
|
|
22
|
+
nn = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if PYTORCH_AVAILABLE:
|
|
26
|
+
|
|
27
|
+
class PyTorchMaterializer(BaseMaterializer):
|
|
28
|
+
"""Materializer for PyTorch models and tensors."""
|
|
29
|
+
|
|
30
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
31
|
+
"""Save PyTorch object to path.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
obj: PyTorch model or tensor
|
|
35
|
+
path: Directory path where object should be saved
|
|
36
|
+
"""
|
|
37
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
if isinstance(obj, nn.Module):
|
|
40
|
+
# Save model
|
|
41
|
+
model_path = path / "model.pt"
|
|
42
|
+
torch.save(obj.state_dict(), model_path)
|
|
43
|
+
|
|
44
|
+
# Save model architecture info
|
|
45
|
+
metadata = {
|
|
46
|
+
"type": "pytorch_model",
|
|
47
|
+
"class_name": obj.__class__.__name__,
|
|
48
|
+
"module": obj.__class__.__module__,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Try to capture model architecture
|
|
52
|
+
with contextlib.suppress(builtins.BaseException):
|
|
53
|
+
metadata["architecture"] = str(obj)
|
|
54
|
+
|
|
55
|
+
with open(path / "metadata.json", "w") as f:
|
|
56
|
+
json.dump(metadata, f, indent=2)
|
|
57
|
+
|
|
58
|
+
elif isinstance(obj, torch.Tensor):
|
|
59
|
+
# Save tensor
|
|
60
|
+
tensor_path = path / "tensor.pt"
|
|
61
|
+
torch.save(obj, tensor_path)
|
|
62
|
+
|
|
63
|
+
metadata = {
|
|
64
|
+
"type": "pytorch_tensor",
|
|
65
|
+
"shape": list(obj.shape),
|
|
66
|
+
"dtype": str(obj.dtype),
|
|
67
|
+
"device": str(obj.device),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
with open(path / "metadata.json", "w") as f:
|
|
71
|
+
json.dump(metadata, f, indent=2)
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
# Generic PyTorch object
|
|
75
|
+
obj_path = path / "object.pt"
|
|
76
|
+
torch.save(obj, obj_path)
|
|
77
|
+
|
|
78
|
+
metadata = {"type": "pytorch_object"}
|
|
79
|
+
with open(path / "metadata.json", "w") as f:
|
|
80
|
+
json.dump(metadata, f, indent=2)
|
|
81
|
+
|
|
82
|
+
def load(self, path: Path) -> Any:
|
|
83
|
+
"""Load PyTorch object from path.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
path: Directory path from which to load object
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Loaded PyTorch object
|
|
90
|
+
"""
|
|
91
|
+
# Load metadata
|
|
92
|
+
metadata_path = path / "metadata.json"
|
|
93
|
+
if metadata_path.exists():
|
|
94
|
+
with open(metadata_path) as f:
|
|
95
|
+
metadata = json.load(f)
|
|
96
|
+
else:
|
|
97
|
+
metadata = {}
|
|
98
|
+
|
|
99
|
+
obj_type = metadata.get("type", "pytorch_object")
|
|
100
|
+
|
|
101
|
+
if obj_type == "pytorch_model":
|
|
102
|
+
# Load model state dict only (user needs to provide architecture)
|
|
103
|
+
model_path = path / "model.pt"
|
|
104
|
+
return torch.load(model_path, weights_only=True)
|
|
105
|
+
|
|
106
|
+
elif obj_type == "pytorch_tensor":
|
|
107
|
+
tensor_path = path / "tensor.pt"
|
|
108
|
+
return torch.load(tensor_path, weights_only=True)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
obj_path = path / "object.pt"
|
|
112
|
+
return torch.load(obj_path, weights_only=False)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def supported_types(cls) -> list[type]:
|
|
116
|
+
"""Return PyTorch types supported by this materializer."""
|
|
117
|
+
return [nn.Module, torch.Tensor]
|
|
118
|
+
|
|
119
|
+
# Auto-register
|
|
120
|
+
register_materializer(PyTorchMaterializer)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# Placeholder when PyTorch not available
|
|
124
|
+
class PyTorchMaterializer(BaseMaterializer):
|
|
125
|
+
"""Placeholder materializer when PyTorch is not installed."""
|
|
126
|
+
|
|
127
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
128
|
+
raise ImportError("PyTorch is not installed. Install with: pip install torch")
|
|
129
|
+
|
|
130
|
+
def load(self, path: Path) -> Any:
|
|
131
|
+
raise ImportError("PyTorch is not installed. Install with: pip install torch")
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def supported_types(cls) -> list[type]:
|
|
135
|
+
return []
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Scikit-learn materializer for model serialization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pickle
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
|
|
9
|
+
import contextlib
|
|
10
|
+
import builtins
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from sklearn.base import BaseEstimator
|
|
14
|
+
|
|
15
|
+
SKLEARN_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
SKLEARN_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if SKLEARN_AVAILABLE:
|
|
21
|
+
|
|
22
|
+
class SklearnMaterializer(BaseMaterializer):
|
|
23
|
+
"""Materializer for scikit-learn models."""
|
|
24
|
+
|
|
25
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
26
|
+
"""Save scikit-learn model to path.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
obj: Scikit-learn model
|
|
30
|
+
path: Directory path where object should be saved
|
|
31
|
+
"""
|
|
32
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Save model using pickle
|
|
35
|
+
model_path = path / "model.pkl"
|
|
36
|
+
with open(model_path, "wb") as f:
|
|
37
|
+
pickle.dump(obj, f)
|
|
38
|
+
|
|
39
|
+
# Save metadata
|
|
40
|
+
metadata = {
|
|
41
|
+
"type": "sklearn_model",
|
|
42
|
+
"class_name": obj.__class__.__name__,
|
|
43
|
+
"module": obj.__class__.__module__,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Capture model parameters
|
|
47
|
+
with contextlib.suppress(builtins.BaseException):
|
|
48
|
+
metadata["params"] = obj.get_params()
|
|
49
|
+
|
|
50
|
+
# Capture feature importance if available
|
|
51
|
+
if hasattr(obj, "feature_importances_"):
|
|
52
|
+
with contextlib.suppress(builtins.BaseException):
|
|
53
|
+
metadata["feature_importances"] = obj.feature_importances_.tolist()
|
|
54
|
+
|
|
55
|
+
# Capture number of features
|
|
56
|
+
if hasattr(obj, "n_features_in_"):
|
|
57
|
+
metadata["n_features"] = int(obj.n_features_in_)
|
|
58
|
+
|
|
59
|
+
# Capture feature names if available
|
|
60
|
+
if hasattr(obj, "feature_names_in_"):
|
|
61
|
+
with contextlib.suppress(builtins.BaseException):
|
|
62
|
+
metadata["feature_names"] = obj.feature_names_in_.tolist()
|
|
63
|
+
|
|
64
|
+
# Capture classes if classifier
|
|
65
|
+
if hasattr(obj, "classes_"):
|
|
66
|
+
with contextlib.suppress(builtins.BaseException):
|
|
67
|
+
metadata["classes"] = obj.classes_.tolist()
|
|
68
|
+
|
|
69
|
+
with open(path / "metadata.json", "w") as f:
|
|
70
|
+
json.dump(metadata, f, indent=2, default=str)
|
|
71
|
+
|
|
72
|
+
def load(self, path: Path) -> Any:
|
|
73
|
+
"""Load scikit-learn model from path.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
path: Directory path from which to load object
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Loaded scikit-learn model
|
|
80
|
+
"""
|
|
81
|
+
model_path = path / "model.pkl"
|
|
82
|
+
|
|
83
|
+
if not model_path.exists():
|
|
84
|
+
raise FileNotFoundError(f"Model file not found at {model_path}")
|
|
85
|
+
|
|
86
|
+
with open(model_path, "rb") as f:
|
|
87
|
+
return pickle.load(f)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def supported_types(cls) -> list[type]:
|
|
91
|
+
"""Return scikit-learn types supported by this materializer."""
|
|
92
|
+
return [BaseEstimator]
|
|
93
|
+
|
|
94
|
+
# Auto-register
|
|
95
|
+
register_materializer(SklearnMaterializer)
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
# Placeholder when scikit-learn not available
|
|
99
|
+
class SklearnMaterializer(BaseMaterializer):
|
|
100
|
+
"""Placeholder materializer when scikit-learn is not installed."""
|
|
101
|
+
|
|
102
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
103
|
+
raise ImportError("Scikit-learn is not installed. Install with: pip install scikit-learn")
|
|
104
|
+
|
|
105
|
+
def load(self, path: Path) -> Any:
|
|
106
|
+
raise ImportError("Scikit-learn is not installed. Install with: pip install scikit-learn")
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def supported_types(cls) -> list[type]:
|
|
110
|
+
return []
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""TensorFlow materializer for model serialization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
|
|
8
|
+
import contextlib
|
|
9
|
+
import builtins
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import tensorflow as tf
|
|
13
|
+
|
|
14
|
+
# Verify TensorFlow has expected attributes
|
|
15
|
+
_ = tf.keras.Model
|
|
16
|
+
_ = tf.Tensor
|
|
17
|
+
_ = tf.Variable
|
|
18
|
+
TENSORFLOW_AVAILABLE = True
|
|
19
|
+
except (ImportError, AttributeError):
|
|
20
|
+
TENSORFLOW_AVAILABLE = False
|
|
21
|
+
tf = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TENSORFLOW_AVAILABLE:
|
|
25
|
+
|
|
26
|
+
class TensorFlowMaterializer(BaseMaterializer):
|
|
27
|
+
"""Materializer for TensorFlow/Keras models."""
|
|
28
|
+
|
|
29
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
30
|
+
"""Save TensorFlow object to path.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
obj: TensorFlow model or tensor
|
|
34
|
+
path: Directory path where object should be saved
|
|
35
|
+
"""
|
|
36
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
if isinstance(obj, tf.keras.Model):
|
|
39
|
+
# Save Keras model in SavedModel format
|
|
40
|
+
model_path = path / "saved_model"
|
|
41
|
+
obj.save(model_path, save_format="tf")
|
|
42
|
+
|
|
43
|
+
# Save metadata
|
|
44
|
+
metadata = {
|
|
45
|
+
"type": "tensorflow_keras_model",
|
|
46
|
+
"class_name": obj.__class__.__name__,
|
|
47
|
+
"input_shape": str(obj.input_shape) if hasattr(obj, "input_shape") else None,
|
|
48
|
+
"output_shape": str(obj.output_shape) if hasattr(obj, "output_shape") else None,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Try to get model config
|
|
52
|
+
with contextlib.suppress(builtins.BaseException):
|
|
53
|
+
metadata["config"] = obj.get_config()
|
|
54
|
+
|
|
55
|
+
with open(path / "metadata.json", "w") as f:
|
|
56
|
+
json.dump(metadata, f, indent=2, default=str)
|
|
57
|
+
|
|
58
|
+
elif isinstance(obj, tf.Tensor):
|
|
59
|
+
# Save tensor as numpy array
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
tensor_path = path / "tensor.npy"
|
|
63
|
+
np.save(tensor_path, obj.numpy())
|
|
64
|
+
|
|
65
|
+
metadata = {
|
|
66
|
+
"type": "tensorflow_tensor",
|
|
67
|
+
"shape": list(obj.shape),
|
|
68
|
+
"dtype": str(obj.dtype),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
with open(path / "metadata.json", "w") as f:
|
|
72
|
+
json.dump(metadata, f, indent=2)
|
|
73
|
+
|
|
74
|
+
elif isinstance(obj, tf.Variable):
|
|
75
|
+
# Save variable
|
|
76
|
+
import numpy as np
|
|
77
|
+
|
|
78
|
+
var_path = path / "variable.npy"
|
|
79
|
+
np.save(var_path, obj.numpy())
|
|
80
|
+
|
|
81
|
+
metadata = {
|
|
82
|
+
"type": "tensorflow_variable",
|
|
83
|
+
"shape": list(obj.shape),
|
|
84
|
+
"dtype": str(obj.dtype),
|
|
85
|
+
"name": obj.name,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
with open(path / "metadata.json", "w") as f:
|
|
89
|
+
json.dump(metadata, f, indent=2)
|
|
90
|
+
|
|
91
|
+
def load(self, path: Path) -> Any:
|
|
92
|
+
"""Load TensorFlow object from path.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
path: Directory path from which to load object
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Loaded TensorFlow object
|
|
99
|
+
"""
|
|
100
|
+
# Load metadata
|
|
101
|
+
metadata_path = path / "metadata.json"
|
|
102
|
+
if metadata_path.exists():
|
|
103
|
+
with open(metadata_path) as f:
|
|
104
|
+
metadata = json.load(f)
|
|
105
|
+
else:
|
|
106
|
+
metadata = {}
|
|
107
|
+
|
|
108
|
+
obj_type = metadata.get("type", "tensorflow_object")
|
|
109
|
+
|
|
110
|
+
if obj_type == "tensorflow_keras_model":
|
|
111
|
+
model_path = path / "saved_model"
|
|
112
|
+
return tf.keras.models.load_model(model_path)
|
|
113
|
+
|
|
114
|
+
elif obj_type == "tensorflow_tensor":
|
|
115
|
+
import numpy as np
|
|
116
|
+
|
|
117
|
+
tensor_path = path / "tensor.npy"
|
|
118
|
+
array = np.load(tensor_path)
|
|
119
|
+
return tf.convert_to_tensor(array)
|
|
120
|
+
|
|
121
|
+
elif obj_type == "tensorflow_variable":
|
|
122
|
+
import numpy as np
|
|
123
|
+
|
|
124
|
+
var_path = path / "variable.npy"
|
|
125
|
+
array = np.load(var_path)
|
|
126
|
+
return tf.Variable(array, name=metadata.get("name"))
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(f"Unknown TensorFlow object type: {obj_type}")
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def supported_types(cls) -> list[type]:
|
|
133
|
+
"""Return TensorFlow types supported by this materializer."""
|
|
134
|
+
return [tf.keras.Model, tf.Tensor, tf.Variable]
|
|
135
|
+
|
|
136
|
+
# Auto-register
|
|
137
|
+
register_materializer(TensorFlowMaterializer)
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
# Placeholder when TensorFlow not available
|
|
141
|
+
class TensorFlowMaterializer(BaseMaterializer):
|
|
142
|
+
"""Placeholder materializer when TensorFlow is not installed."""
|
|
143
|
+
|
|
144
|
+
def save(self, obj: Any, path: Path) -> None:
|
|
145
|
+
raise ImportError("TensorFlow is not installed. Install with: pip install tensorflow")
|
|
146
|
+
|
|
147
|
+
def load(self, path: Path) -> Any:
|
|
148
|
+
raise ImportError("TensorFlow is not installed. Install with: pip install tensorflow")
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def supported_types(cls) -> list[type]:
|
|
152
|
+
return []
|