apache-hamilton 1.90.0.dev0__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.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
from collections.abc import Collection
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from kedro.io import DataCatalog
|
|
23
|
+
|
|
24
|
+
from hamilton import registry
|
|
25
|
+
from hamilton.io.data_adapters import DataLoader, DataSaver
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass
|
|
29
|
+
class KedroSaver(DataSaver):
|
|
30
|
+
"""Use Kedro DataCatalog and Dataset to save results
|
|
31
|
+
ref: https://docs.kedro.org/en/stable/data/advanced_data_catalog_usage.html
|
|
32
|
+
|
|
33
|
+
.. code-block:: python
|
|
34
|
+
|
|
35
|
+
from kedro.framework.session import KedroSession
|
|
36
|
+
|
|
37
|
+
with KedroSession.create() as session:
|
|
38
|
+
context = session.load_context()
|
|
39
|
+
catalog = context.catalog
|
|
40
|
+
|
|
41
|
+
dr.materialize(
|
|
42
|
+
to.kedro(
|
|
43
|
+
id="my_dataset__kedro",
|
|
44
|
+
dependencies=["my_dataset"],
|
|
45
|
+
dataset_name="my_dataset",
|
|
46
|
+
catalog=catalog,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
dataset_name: str
|
|
52
|
+
catalog: DataCatalog
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def applicable_types(cls) -> Collection[type]:
|
|
56
|
+
return [Any]
|
|
57
|
+
|
|
58
|
+
def save_data(self, data: Any) -> dict[str, Any]:
|
|
59
|
+
self.catalog.save(self.dataset_name, data)
|
|
60
|
+
return dict(success=True)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def name(cls) -> str:
|
|
64
|
+
return "kedro"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclasses.dataclass
|
|
68
|
+
class KedroLoader(DataLoader):
|
|
69
|
+
"""Use Kedro DataCatalog and Dataset to load data
|
|
70
|
+
ref: https://docs.kedro.org/en/stable/data/advanced_data_catalog_usage.html
|
|
71
|
+
|
|
72
|
+
.. code-block:: python
|
|
73
|
+
|
|
74
|
+
from kedro.framework.session import KedroSession
|
|
75
|
+
|
|
76
|
+
with KedroSession.create() as session:
|
|
77
|
+
context = session.load_context()
|
|
78
|
+
catalog = context.catalog
|
|
79
|
+
|
|
80
|
+
dr.materialize(
|
|
81
|
+
from_.kedro(
|
|
82
|
+
target="input_table",
|
|
83
|
+
dataset_name="input_table",
|
|
84
|
+
catalog=catalog
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
dataset_name: str
|
|
90
|
+
catalog: DataCatalog
|
|
91
|
+
version: str | None = None
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def applicable_types(cls) -> Collection[type]:
|
|
95
|
+
return [Any]
|
|
96
|
+
|
|
97
|
+
def load_data(self, type_: type) -> tuple[Any, dict[str, Any]]:
|
|
98
|
+
data = self.catalog.load(self.dataset_name, self.version)
|
|
99
|
+
metadata = dict(dataset_name=self.dataset_name, version=self.version)
|
|
100
|
+
return data, metadata
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def name(cls) -> str:
|
|
104
|
+
return "kedro"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def register_data_loaders():
|
|
108
|
+
for loader in [
|
|
109
|
+
KedroSaver,
|
|
110
|
+
KedroLoader,
|
|
111
|
+
]:
|
|
112
|
+
registry.register_adapter(loader)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
register_data_loaders()
|
|
116
|
+
|
|
117
|
+
COLUMN_FRIENDLY_DF_TYPE = False
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
from collections.abc import Collection
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Literal, Union
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import lightgbm
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
raise NotImplementedError("LightGBM is not installed.") from e
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from hamilton import registry
|
|
30
|
+
from hamilton.io import utils
|
|
31
|
+
from hamilton.io.data_adapters import DataLoader, DataSaver
|
|
32
|
+
|
|
33
|
+
LIGHTGBM_MODEL_TYPES = [lightgbm.LGBMModel, lightgbm.Booster, lightgbm.CVBooster]
|
|
34
|
+
LIGHTGBM_MODEL_TYPES_ANNOTATION = Union[lightgbm.LGBMModel, lightgbm.Booster, lightgbm.CVBooster]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclasses.dataclass
|
|
38
|
+
class LightGBMFileWriter(DataSaver):
|
|
39
|
+
"""Write LighGBM models and boosters to a file"""
|
|
40
|
+
|
|
41
|
+
path: str | Path
|
|
42
|
+
num_iteration: int | None = None
|
|
43
|
+
start_iteration: int = 0
|
|
44
|
+
importance_type: Literal["split", "gain"] = "split"
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def applicable_types(cls) -> Collection[type]:
|
|
48
|
+
return LIGHTGBM_MODEL_TYPES
|
|
49
|
+
|
|
50
|
+
def save_data(self, data: LIGHTGBM_MODEL_TYPES_ANNOTATION) -> dict[str, Any]:
|
|
51
|
+
if isinstance(data, lightgbm.LGBMModel):
|
|
52
|
+
data = data.booster_
|
|
53
|
+
|
|
54
|
+
data.save_model(
|
|
55
|
+
filename=self.path,
|
|
56
|
+
num_iteration=self.num_iteration,
|
|
57
|
+
start_iteration=self.start_iteration,
|
|
58
|
+
importance_type=self.importance_type,
|
|
59
|
+
)
|
|
60
|
+
return utils.get_file_metadata(self.path)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def name(cls) -> str:
|
|
64
|
+
return "file"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclasses.dataclass
|
|
68
|
+
class LightGBMFileReader(DataLoader):
|
|
69
|
+
"""Load LighGBM models and boosters from a file"""
|
|
70
|
+
|
|
71
|
+
path: str | Path
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def applicable_types(cls) -> Collection[type]:
|
|
75
|
+
return LIGHTGBM_MODEL_TYPES
|
|
76
|
+
|
|
77
|
+
def load_data(
|
|
78
|
+
self, type_: type
|
|
79
|
+
) -> tuple[lightgbm.Booster | lightgbm.CVBooster, dict[str, Any]]:
|
|
80
|
+
model = type_(model_file=self.path)
|
|
81
|
+
metadata = utils.get_file_metadata(self.path)
|
|
82
|
+
return model, metadata
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def name(cls) -> str:
|
|
86
|
+
return "file"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def register_data_loaders():
|
|
90
|
+
for loader in [
|
|
91
|
+
LightGBMFileReader,
|
|
92
|
+
LightGBMFileWriter,
|
|
93
|
+
]:
|
|
94
|
+
registry.register_adapter(loader)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
register_data_loaders()
|
|
98
|
+
|
|
99
|
+
COLUMN_FRIENDLY_DF_TYPE = False
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
from collections.abc import Collection
|
|
20
|
+
from os import PathLike
|
|
21
|
+
from typing import IO, Any
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from matplotlib.artist import Artist
|
|
25
|
+
from matplotlib.figure import Figure
|
|
26
|
+
from matplotlib.transforms import Bbox
|
|
27
|
+
except ImportError as e:
|
|
28
|
+
raise NotImplementedError("Matplotlib is not installed.") from e
|
|
29
|
+
|
|
30
|
+
from hamilton import registry
|
|
31
|
+
from hamilton.io import utils
|
|
32
|
+
from hamilton.io.data_adapters import DataSaver
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class MatplotlibWriter(DataSaver):
|
|
37
|
+
"""Write Matplotlib figure as static image format
|
|
38
|
+
ref: https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
path: str | PathLike | IO
|
|
42
|
+
dpi: float | str | None = None
|
|
43
|
+
format: str | None = None
|
|
44
|
+
metadata: dict | None = None
|
|
45
|
+
bbox_inches: str | Bbox | None = None
|
|
46
|
+
pad_inches: float | str | None = None
|
|
47
|
+
facecolor: str | float | tuple | None = None
|
|
48
|
+
edgecolor: str | float | tuple | None = None
|
|
49
|
+
backend: str | None = None
|
|
50
|
+
orientation: str | None = None
|
|
51
|
+
papertype: str | None = None
|
|
52
|
+
transparent: bool | None = None
|
|
53
|
+
bbox_extra_artists: list[Artist] | None = None
|
|
54
|
+
pil_kwargs: dict | None = None
|
|
55
|
+
|
|
56
|
+
def _get_saving_kwargs(self) -> dict:
|
|
57
|
+
kwargs = {}
|
|
58
|
+
if self.format is not None:
|
|
59
|
+
kwargs["format"] = self.format
|
|
60
|
+
if self.metadata is not None:
|
|
61
|
+
kwargs["metadata"] = self.metadata
|
|
62
|
+
if self.bbox_inches is not None:
|
|
63
|
+
kwargs["bbox_inches"] = self.bbox_inches
|
|
64
|
+
if self.pad_inches is not None:
|
|
65
|
+
kwargs["pad_inches"] = self.pad_inches
|
|
66
|
+
if self.facecolor is not None:
|
|
67
|
+
kwargs["facecolor"] = self.facecolor
|
|
68
|
+
if self.edgecolor is not None:
|
|
69
|
+
kwargs["edgecolor"] = self.edgecolor
|
|
70
|
+
if self.backend is not None:
|
|
71
|
+
kwargs["backend"] = self.backend
|
|
72
|
+
if self.orientation is not None:
|
|
73
|
+
kwargs["orientation"] = self.orientation
|
|
74
|
+
if self.papertype is not None:
|
|
75
|
+
kwargs["papertype"] = self.papertype
|
|
76
|
+
if self.transparent is not None:
|
|
77
|
+
kwargs["transparent"] = self.transparent
|
|
78
|
+
if self.bbox_extra_artists is not None:
|
|
79
|
+
kwargs["bbox_extra_artists"] = self.bbox_extra_artists
|
|
80
|
+
if self.pil_kwargs is not None:
|
|
81
|
+
kwargs["pil_kwargs"] = self.pil_kwargs
|
|
82
|
+
|
|
83
|
+
return kwargs
|
|
84
|
+
|
|
85
|
+
def save_data(self, data: Figure) -> dict[str, Any]:
|
|
86
|
+
data.savefig(fname=self.path, **self._get_saving_kwargs())
|
|
87
|
+
# TODO make utils.get_file_metadata() safer for when self.path is IO type
|
|
88
|
+
return utils.get_file_metadata(self.path)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def applicable_types(cls) -> Collection[type]:
|
|
92
|
+
return [Figure]
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def name(cls) -> str:
|
|
96
|
+
return "plt"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_data_savers():
|
|
100
|
+
for saver in [
|
|
101
|
+
MatplotlibWriter,
|
|
102
|
+
]:
|
|
103
|
+
registry.register_adapter(saver)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
register_data_savers()
|
|
107
|
+
|
|
108
|
+
COLUMN_FRIENDLY_DF_TYPE = False
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import pathlib
|
|
20
|
+
from collections.abc import Collection
|
|
21
|
+
from types import ModuleType
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import mlflow
|
|
26
|
+
except ImportError as e:
|
|
27
|
+
raise NotImplementedError("MLFlow is not installed.") from e
|
|
28
|
+
|
|
29
|
+
from hamilton import registry
|
|
30
|
+
from hamilton.io.data_adapters import DataLoader, DataSaver
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass
|
|
34
|
+
class MLFlowModelSaver(DataSaver):
|
|
35
|
+
"""Save model to the MLFlow tracking server using `.log_model()`
|
|
36
|
+
|
|
37
|
+
:param path: Run relative path to store model. Will constitute the model URI.
|
|
38
|
+
:param register_as: If not None, register the model under the specified name.
|
|
39
|
+
:param flavor: Library format to save the model (sklearn, xgboost, etc.). Automatically inferred if None.
|
|
40
|
+
:param run_id: Log model to a specific run. Leave to `None` if using the `MLFlowTracker`
|
|
41
|
+
:param mlflow_kwargs: Arguments for `.log_model()`. Can be flavor-specific.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
path: str | pathlib.Path = "model"
|
|
45
|
+
register_as: str | None = None
|
|
46
|
+
flavor: str | ModuleType | None = None
|
|
47
|
+
run_id: str | None = None
|
|
48
|
+
mlflow_kwargs: dict[str, Any] = None
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
self.mlflow_kwargs = self.mlflow_kwargs or {}
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def name(cls) -> str:
|
|
55
|
+
return "mlflow"
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def applicable_types(cls) -> Collection[type]:
|
|
59
|
+
return [Any]
|
|
60
|
+
|
|
61
|
+
def save_data(self, data) -> dict[str, Any]:
|
|
62
|
+
if self.flavor:
|
|
63
|
+
flavor = self.flavor
|
|
64
|
+
else:
|
|
65
|
+
# infer the flavor from the base module of the data class
|
|
66
|
+
# for example, extract `sklearn` from `sklearn.linear_model._base`
|
|
67
|
+
flavor, _, _ = data.__module__.partition(".")
|
|
68
|
+
|
|
69
|
+
# pass flavor as module, this suports standard flavors `like mlflow.sklearn`
|
|
70
|
+
# but also supports custom flavors like `my_flavor` that implements save(), log(), load()
|
|
71
|
+
if isinstance(flavor, ModuleType):
|
|
72
|
+
flavor_module = flavor
|
|
73
|
+
else:
|
|
74
|
+
# retrieve the `mlflow.FLAVOR` submodule to `.log_model()`
|
|
75
|
+
try:
|
|
76
|
+
flavor_module = getattr(mlflow, flavor)
|
|
77
|
+
except ImportError as e:
|
|
78
|
+
raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") from e
|
|
79
|
+
|
|
80
|
+
# handle `run_id` and active run conflicts
|
|
81
|
+
if mlflow.active_run() and self.run_id:
|
|
82
|
+
if mlflow.active_run().info.run_id != self.run_id:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
"The MLFlowModelSaver `run_id` doesn't match the active `run_id`\n",
|
|
85
|
+
"Set `run_id=None` to save to the active MLFlow run.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# save to active run
|
|
89
|
+
if mlflow.active_run():
|
|
90
|
+
model_info = flavor_module.log_model(data, self.path, **self.mlflow_kwargs)
|
|
91
|
+
# create a run with `run_id` and save to it
|
|
92
|
+
else:
|
|
93
|
+
with mlflow.start_run(run_id=self.run_id):
|
|
94
|
+
model_info = flavor_module.log_model(data, self.path, **self.mlflow_kwargs)
|
|
95
|
+
|
|
96
|
+
# create metadata from ModelInfo object
|
|
97
|
+
metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()}
|
|
98
|
+
|
|
99
|
+
if self.register_as:
|
|
100
|
+
model_version = mlflow.register_model(
|
|
101
|
+
model_uri=metadata["model_uri"], name=self.register_as
|
|
102
|
+
)
|
|
103
|
+
# update metadata with the registered ModelVersion
|
|
104
|
+
# there's a contract between this key and the MLFlowTracker's
|
|
105
|
+
# post_node_execute() reads this metadata
|
|
106
|
+
metadata["registered_model"] = {
|
|
107
|
+
k.strip("_"): v for k, v in model_version.__dict__.items()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return metadata
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass
|
|
114
|
+
class MLFlowModelLoader(DataLoader):
|
|
115
|
+
"""Load model from the MLFlow tracking server or model registry using .load_model()
|
|
116
|
+
You can pass a model URI or the necessary metadata to retrieve the model
|
|
117
|
+
|
|
118
|
+
:param model_uri: Model location starting as `runs:/` for tracking or `models:/` for registry
|
|
119
|
+
:param mode: `tracking` or registry`. tracking needs `run_id` and `path`. registry needs `model_name` and `version` or `version_alias`.
|
|
120
|
+
:param run_id: Run id of the model on the tracking server
|
|
121
|
+
:param path: Run relative path where the model is stored
|
|
122
|
+
:param model_name: Name of the registered model (equivalent to `register_as` in model saver)
|
|
123
|
+
:param version: Version of the registered model. Can pass as string `v1` or integer `1`
|
|
124
|
+
:param version_alias: Version alias of the registered model. Specify either this or `version`
|
|
125
|
+
:param flavor: Library format to load the model (sklearn, xgboost, etc.). Automatically inferred if None.
|
|
126
|
+
:param mlflow_kwargs: Arguments for `.load_model()`. Can be flavor-specific.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
model_uri: str | None = None
|
|
130
|
+
mode: Literal["tracking", "registry"] = "tracking"
|
|
131
|
+
run_id: str | None = None
|
|
132
|
+
path: str | pathlib.Path = "model"
|
|
133
|
+
model_name: str | None = None
|
|
134
|
+
version: str | int | None = None
|
|
135
|
+
version_alias: str | None = None
|
|
136
|
+
flavor: ModuleType | str | None = None
|
|
137
|
+
mlflow_kwargs: dict[str, Any] = None
|
|
138
|
+
|
|
139
|
+
# __post_init__ is required to set kwargs as empty dict because
|
|
140
|
+
# can't set: kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
141
|
+
# otherwise raises `InvalidDecoratorException` because materializer factory check
|
|
142
|
+
# for all params being set and `kwargs` would be unset until instantiation.
|
|
143
|
+
def __post_init__(self):
|
|
144
|
+
self.mlflow_kwargs = self.mlflow_kwargs or {}
|
|
145
|
+
|
|
146
|
+
if self.model_uri:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if self.mode == "tracking":
|
|
150
|
+
if (not self.run_id) or (not self.path):
|
|
151
|
+
raise ValueError("Using `mode='tracking'` requires passing `run_id` and `path`")
|
|
152
|
+
|
|
153
|
+
self.model_uri = f"runs:/{self.run_id}/{self.path}"
|
|
154
|
+
|
|
155
|
+
elif self.mode == "registry":
|
|
156
|
+
if not self.model_name:
|
|
157
|
+
raise ValueError("Using `mode='registry` requires passing `model_name`")
|
|
158
|
+
|
|
159
|
+
if not (bool(self.version) ^ bool(self.version_alias)):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"If using `mode='registry'` requires passing `version` OR `version_alias"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if self.version:
|
|
165
|
+
self.model_uri = f"models:/{self.model_name}/{self.version}"
|
|
166
|
+
elif self.version_alias:
|
|
167
|
+
self.model_uri = f"models:/{self.model_name}@{self.version_alias}"
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def name(cls) -> str:
|
|
171
|
+
return "mlflow"
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def applicable_types(cls) -> Collection[type]:
|
|
175
|
+
return [Any]
|
|
176
|
+
|
|
177
|
+
def load_data(self, type_: type) -> tuple[Any, dict[str, Any]]:
|
|
178
|
+
model_info = mlflow.models.model.get_model_info(self.model_uri)
|
|
179
|
+
metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()}
|
|
180
|
+
|
|
181
|
+
flavor = self.flavor
|
|
182
|
+
# if flavor not explicitly passed, retrieve flavor from the ModelInfo
|
|
183
|
+
if flavor is None:
|
|
184
|
+
# prioritize the library specific flavor. Default to `pyfunc` if none available.
|
|
185
|
+
try:
|
|
186
|
+
flavor = next(f for f in metadata["flavors"].keys() if f != "python_function")
|
|
187
|
+
except StopIteration:
|
|
188
|
+
flavor = "pyfunc"
|
|
189
|
+
|
|
190
|
+
# pass flavor as module, this suports standard flavors `like mlflow.sklearn`
|
|
191
|
+
# but also supports custom flavors like `my_flavor` that implements save(), log(), load()
|
|
192
|
+
if isinstance(flavor, ModuleType):
|
|
193
|
+
flavor_module = flavor
|
|
194
|
+
else:
|
|
195
|
+
# retrieve the `mlflow.FLAVOR` submodule to `.log_model()`
|
|
196
|
+
try:
|
|
197
|
+
flavor_module = getattr(mlflow, flavor)
|
|
198
|
+
except ImportError as e:
|
|
199
|
+
raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") from e
|
|
200
|
+
|
|
201
|
+
model = flavor_module.load_model(model_uri=self.model_uri, **self.mlflow_kwargs)
|
|
202
|
+
return model, metadata
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def register_data_loaders():
|
|
206
|
+
"""Function to register the data loaders for this extension."""
|
|
207
|
+
for loader in [
|
|
208
|
+
MLFlowModelSaver,
|
|
209
|
+
MLFlowModelLoader,
|
|
210
|
+
]:
|
|
211
|
+
registry.register_adapter(loader)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
register_data_loaders()
|
|
215
|
+
|
|
216
|
+
COLUMN_FRIENDLY_DF_TYPE = False
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import pathlib
|
|
20
|
+
from collections.abc import Collection
|
|
21
|
+
from typing import IO, Any
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import numpy as np
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
raise NotImplementedError("Numpy is not installed.") from e
|
|
27
|
+
|
|
28
|
+
from typing import Literal
|
|
29
|
+
|
|
30
|
+
from hamilton import registry
|
|
31
|
+
from hamilton.io import utils
|
|
32
|
+
from hamilton.io.data_adapters import DataLoader, DataSaver
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class NumpyNpyWriter(DataSaver):
|
|
37
|
+
"""Write Numpy multidimensional arrays to custom .npy format
|
|
38
|
+
ref: https://numpy.org/doc/stable/reference/routines.io.html
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
path: str | pathlib.Path | IO
|
|
42
|
+
allow_pickle: bool | None = None
|
|
43
|
+
fix_imports: bool | None = None
|
|
44
|
+
|
|
45
|
+
def save_data(self, data: np.ndarray) -> dict[str, Any]:
|
|
46
|
+
kwargs = dict(file=self.path, arr=data, allow_pickle=self.allow_pickle)
|
|
47
|
+
if np.__version__ < "2.4" and self.fix_imports is not None:
|
|
48
|
+
kwargs["fix_imports"] = self.fix_imports
|
|
49
|
+
np.save(**kwargs)
|
|
50
|
+
return utils.get_file_metadata(self.path)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def applicable_types(cls) -> Collection[type]:
|
|
54
|
+
return [np.ndarray]
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def name(cls) -> str:
|
|
58
|
+
return "npy"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclasses.dataclass
|
|
62
|
+
class NumpyNpyReader(DataLoader):
|
|
63
|
+
"""Read Numpy multidimensional arrays from custom .npy format
|
|
64
|
+
ref: https://numpy.org/doc/stable/reference/routines.io.html
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
path: str | pathlib.Path | IO
|
|
68
|
+
mmap_mode: str | None = None
|
|
69
|
+
allow_pickle: bool | None = None
|
|
70
|
+
fix_imports: bool | None = None
|
|
71
|
+
encoding: Literal["ASCII", "latin1", "bytes"] = "ASCII"
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def applicable_types(cls) -> Collection[type]:
|
|
75
|
+
return [np.ndarray]
|
|
76
|
+
|
|
77
|
+
def load_data(self, type_: type) -> tuple[np.ndarray, dict[str, Any]]:
|
|
78
|
+
kwargs = dict(
|
|
79
|
+
file=self.path,
|
|
80
|
+
mmap_mode=self.mmap_mode,
|
|
81
|
+
allow_pickle=self.allow_pickle,
|
|
82
|
+
encoding=self.encoding,
|
|
83
|
+
)
|
|
84
|
+
if np.__version__ < "2.4" and self.fix_imports is not None:
|
|
85
|
+
kwargs["fix_imports"] = self.fix_imports
|
|
86
|
+
array = np.load(**kwargs)
|
|
87
|
+
metadata = utils.get_file_metadata(self.path)
|
|
88
|
+
return array, metadata
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def name(cls) -> str:
|
|
92
|
+
return "npy"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def register_data_loaders():
|
|
96
|
+
for loader in [
|
|
97
|
+
NumpyNpyWriter,
|
|
98
|
+
NumpyNpyReader,
|
|
99
|
+
]:
|
|
100
|
+
registry.register_adapter(loader)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
register_data_loaders()
|
|
104
|
+
|
|
105
|
+
COLUMN_FRIENDLY_DF_TYPE = False
|