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.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. 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