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,233 @@
|
|
|
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
|
+
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from hamilton.function_modifiers import (
|
|
22
|
+
UpstreamDependency,
|
|
23
|
+
base,
|
|
24
|
+
parameterize_extract_columns,
|
|
25
|
+
source,
|
|
26
|
+
value,
|
|
27
|
+
)
|
|
28
|
+
from hamilton.function_modifiers.expanders import ParameterizedExtract
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_dep_type(dep_type: str) -> UpstreamDependency:
|
|
32
|
+
"""Converts dependency type to the type known by function_modifier"""
|
|
33
|
+
if dep_type == "out":
|
|
34
|
+
return None
|
|
35
|
+
if dep_type == "value":
|
|
36
|
+
return value
|
|
37
|
+
if dep_type == "source":
|
|
38
|
+
return source
|
|
39
|
+
raise ValueError(f"Invalid dep type: {dep_type}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_index_levels(index: pd.MultiIndex) -> list[list]:
|
|
43
|
+
out = [[] for _ in index[0]]
|
|
44
|
+
for specific_index in index:
|
|
45
|
+
for i, key in enumerate(specific_index):
|
|
46
|
+
out[i].append(key)
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_df_parameterization(parameterization: pd.DataFrame):
|
|
51
|
+
# TODO -- validate that its a multi-index
|
|
52
|
+
columns = _get_index_levels(parameterization.columns)
|
|
53
|
+
if (not len(columns) == 2) or "out" not in columns[1]:
|
|
54
|
+
raise base.InvalidDecoratorException(
|
|
55
|
+
"Decorator must have a double-index -- first index should be a "
|
|
56
|
+
"list of {output, source, value} strs. Second must be a list of "
|
|
57
|
+
"arguments in your function."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _convert_params_from_df(parameterization: pd.DataFrame) -> list[ParameterizedExtract]:
|
|
62
|
+
_validate_df_parameterization(parameterization)
|
|
63
|
+
args, dep_types = _get_index_levels(parameterization.columns)
|
|
64
|
+
dep_types_converted = [_get_dep_type(val) for val in dep_types]
|
|
65
|
+
out = []
|
|
66
|
+
for _, column_set in parameterization.iterrows():
|
|
67
|
+
parameterization = {
|
|
68
|
+
arg: dep_type(col_value)
|
|
69
|
+
for arg, col_value, dep_type in zip(args, column_set, dep_types_converted, strict=False)
|
|
70
|
+
if dep_type is not None
|
|
71
|
+
}
|
|
72
|
+
extracted_columns = [
|
|
73
|
+
col for col, dep_type in zip(column_set, dep_types, strict=False) if dep_type == "out"
|
|
74
|
+
]
|
|
75
|
+
out.append(ParameterizedExtract(tuple(extracted_columns), parameterization))
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class parameterize_frame(parameterize_extract_columns):
|
|
80
|
+
"""EXPERIMENTAL! Instantiates a parameterize_extract decorator using a dataframe to specify a set of extracts + \
|
|
81
|
+
parameterizations.
|
|
82
|
+
|
|
83
|
+
This is an experimental decorator and the API may change in the future; please provide feedback \
|
|
84
|
+
whether this API does or does not work for you.
|
|
85
|
+
|
|
86
|
+
:param parameterization: Parameterization dataframe. See below.
|
|
87
|
+
|
|
88
|
+
This is of a specific shape:
|
|
89
|
+
|
|
90
|
+
1. Index - Level 0: list of parameter names
|
|
91
|
+
2. Index - Level 1: types of things to inject, either:
|
|
92
|
+
|
|
93
|
+
- "out" (meaning this is an output),
|
|
94
|
+
- "value" (meaning this is a literal value)
|
|
95
|
+
- "source" (meaning this node comes from an upstream value)
|
|
96
|
+
|
|
97
|
+
3. Contents:
|
|
98
|
+
|
|
99
|
+
- Each row corresponds to the index. Each of these corresponds to an output node from this.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Note your function has to take in the column-names and output a dataframe with those names -- \
|
|
103
|
+
we will likely change it so that's not the case, and it can just use the position of the columns.
|
|
104
|
+
|
|
105
|
+
Example usage:
|
|
106
|
+
|
|
107
|
+
.. code-block:: python
|
|
108
|
+
|
|
109
|
+
from hamilton.experimental.decorators.parameterize_frame import parameterize_frame
|
|
110
|
+
df = pd.DataFrame(
|
|
111
|
+
[
|
|
112
|
+
["outseries1a", "outseries2a", "inseries1a", "inseries2a", 5.0],
|
|
113
|
+
["outseries1b", "outseries2b", "inseries1b", "inseries2b", 0.2],
|
|
114
|
+
],
|
|
115
|
+
# specify column names corresponding to function arguments and
|
|
116
|
+
# if outputting multiple columns, output dataframe columns.
|
|
117
|
+
columns=[
|
|
118
|
+
["output1", "output2", "input1", "input2", "input3"],
|
|
119
|
+
["out", "out", "source", "source", "value"],
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
@parameterize_frame(df)
|
|
123
|
+
def my_func(
|
|
124
|
+
input1: pd.Series, input2: pd.Series, input3: float
|
|
125
|
+
) -> pd.DataFrame:
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, parameterization: pd.DataFrame):
|
|
131
|
+
super(parameterize_frame, self).__init__(*_convert_params_from_df(parameterization))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Examples below
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
df = pd.DataFrame(
|
|
137
|
+
[
|
|
138
|
+
["outseries1a", "outseries2a", "inseries1a", "inseries2a", 5.0],
|
|
139
|
+
["outseries1b", "outseries2b", "inseries1b", "inseries2b", 0.2],
|
|
140
|
+
# ...
|
|
141
|
+
],
|
|
142
|
+
# Have to switch as indices have to be unique
|
|
143
|
+
columns=[
|
|
144
|
+
[
|
|
145
|
+
"output1",
|
|
146
|
+
"output2",
|
|
147
|
+
"input1",
|
|
148
|
+
"input2",
|
|
149
|
+
"input3",
|
|
150
|
+
],
|
|
151
|
+
# configure whether column is source or value and also whether it's input ("source", "value") or output ("out")
|
|
152
|
+
["out", "out", "source", "source", "value"],
|
|
153
|
+
],
|
|
154
|
+
) # specify column names (corresponding to function arguments and (if outputting multiple columns) output dataframe columns)
|
|
155
|
+
|
|
156
|
+
@parameterize_frame(df)
|
|
157
|
+
def my_func(input1: pd.Series, input2: pd.Series, input3: float) -> pd.DataFrame:
|
|
158
|
+
return pd.DataFrame(
|
|
159
|
+
[input1 * input2 * input3, input1 + input2 + input3]
|
|
160
|
+
) # if there's a single column it could maybe just return a series instead and pick up the name from the first column of the dataframe
|
|
161
|
+
|
|
162
|
+
@parameterize_extract_columns(
|
|
163
|
+
ParameterizedExtract(
|
|
164
|
+
("outseries1a", "outseries2a"),
|
|
165
|
+
{"input1": source("inseries1a"), "input2": source("inseries2a"), "input3": value(5.0)},
|
|
166
|
+
),
|
|
167
|
+
ParameterizedExtract(
|
|
168
|
+
("outseries1b", "outseries2b"),
|
|
169
|
+
{"input1": source("inseries1b"), "input2": source("inseries2b"), "input3": value(0.2)},
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
def my_func_parameterized_extract(
|
|
173
|
+
input1: pd.Series, input2: pd.Series, input3: float
|
|
174
|
+
) -> pd.DataFrame:
|
|
175
|
+
print("running my_func_parameterized_extract")
|
|
176
|
+
return pd.concat([input1 * input2 * input3, input1 + input2 + input3], axis=1)
|
|
177
|
+
|
|
178
|
+
my_func_parameterized_extract.decorated = "false"
|
|
179
|
+
|
|
180
|
+
# Test by running the @parameterized_extract decorator
|
|
181
|
+
from hamilton.ad_hoc_utils import create_temporary_module
|
|
182
|
+
from hamilton.driver import Driver
|
|
183
|
+
|
|
184
|
+
dr = Driver({}, create_temporary_module(my_func_parameterized_extract))
|
|
185
|
+
dr.visualize_execution(
|
|
186
|
+
final_vars=["outseries1a", "outseries1b", "outseries2a", "outseries2b"],
|
|
187
|
+
output_file_path="./out1.pdf",
|
|
188
|
+
render_kwargs={},
|
|
189
|
+
inputs={
|
|
190
|
+
"inseries1a": pd.Series([1, 2]),
|
|
191
|
+
"inseries1b": pd.Series([2, 3]),
|
|
192
|
+
"inseries2a": pd.Series([3, 4]),
|
|
193
|
+
"inseries2b": pd.Series([4, 5]),
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
df_1 = dr.execute(
|
|
198
|
+
final_vars=["outseries1a", "outseries1b", "outseries2a", "outseries2b"],
|
|
199
|
+
# final_vars=["outseries1a", "outseries2a"],
|
|
200
|
+
inputs={
|
|
201
|
+
"inseries1a": pd.Series([1, 2]),
|
|
202
|
+
"inseries1b": pd.Series([2, 3]),
|
|
203
|
+
"inseries2a": pd.Series([3, 4]),
|
|
204
|
+
"inseries2b": pd.Series([4, 5]),
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
print(df_1)
|
|
208
|
+
|
|
209
|
+
# Test by running the @parameterized_extract decorator
|
|
210
|
+
dr = Driver({}, create_temporary_module(my_func))
|
|
211
|
+
dr.visualize_execution(
|
|
212
|
+
final_vars=["outseries1a", "outseries1b", "outseries2a", "outseries2b"],
|
|
213
|
+
output_file_path="./out2.pdf",
|
|
214
|
+
render_kwargs={},
|
|
215
|
+
inputs={
|
|
216
|
+
"inseries1a": pd.Series([1, 2]),
|
|
217
|
+
"inseries1b": pd.Series([2, 3]),
|
|
218
|
+
"inseries2a": pd.Series([3, 4]),
|
|
219
|
+
"inseries2b": pd.Series([4, 5]),
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
df_2 = dr.execute(
|
|
224
|
+
final_vars=["outseries1a", "outseries1b", "outseries2a", "outseries2b"],
|
|
225
|
+
# final_vars=["outseries1a", "outseries2a"],
|
|
226
|
+
inputs={
|
|
227
|
+
"inseries1a": pd.Series([1, 2]),
|
|
228
|
+
"inseries1b": pd.Series([2, 3]),
|
|
229
|
+
"inseries2a": pd.Series([3, 4]),
|
|
230
|
+
"inseries2b": pd.Series([4, 5]),
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
print(df_2)
|
|
@@ -0,0 +1,29 @@
|
|
|
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 logging
|
|
19
|
+
|
|
20
|
+
import hamilton.async_driver
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
logger.warning(
|
|
25
|
+
"This module is deprecated and will be removed in Hamilton 2.0 "
|
|
26
|
+
"Please use `hamilton.async_driver` instead. "
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
AsyncDriver = hamilton.async_driver.AsyncDriver
|
|
@@ -0,0 +1,413 @@
|
|
|
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 json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import pickle
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from functools import singledispatch
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import typing_inspect
|
|
27
|
+
|
|
28
|
+
from hamilton.base import SimplePythonGraphAdapter
|
|
29
|
+
from hamilton.node import Node
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger.warning(
|
|
35
|
+
"The module `hamilton.experimental.h_cache` and the class `CachingGraphAdapter `"
|
|
36
|
+
"are deprecated and will be removed in Hamilton 2.0. "
|
|
37
|
+
"Consider enabling the core caching feature via `Builder.with_cache()`. "
|
|
38
|
+
"This might not be 1-to-1 replacement, so please reach out if there are missing features. "
|
|
39
|
+
"See https://hamilton.apache.org/concepts/caching/ to learn more."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
Base SERDE functions.
|
|
45
|
+
|
|
46
|
+
Basic format is:
|
|
47
|
+
@singledispatch
|
|
48
|
+
def write_<format>(data: object, filepath: str, name: str) -> None:
|
|
49
|
+
|
|
50
|
+
@singledispatch
|
|
51
|
+
def read_<format>(data: object, filepath: str) -> Any:
|
|
52
|
+
|
|
53
|
+
Functions should register themselves with the appropriate type.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@singledispatch
|
|
58
|
+
def write_feather(data: object, filepath: str, name: str) -> None:
|
|
59
|
+
"""Writes data to a feather file."""
|
|
60
|
+
raise NotImplementedError(f"No feather writer for type {type(data)} registered.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@singledispatch
|
|
64
|
+
def read_feather(data: object, filepath: str) -> Any:
|
|
65
|
+
"""Reads from a feather file"""
|
|
66
|
+
raise NotImplementedError(f"No feather reader for type {type(data)} registered.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@singledispatch
|
|
70
|
+
def write_parquet(data: object, filepath: str, name: str) -> None:
|
|
71
|
+
"""Writes data to a parquet file."""
|
|
72
|
+
raise NotImplementedError(f"No parquet writer for type {type(data)} registered.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@singledispatch
|
|
76
|
+
def read_parquet(data: object, filepath: str) -> Any:
|
|
77
|
+
"""Reads from a parquet file"""
|
|
78
|
+
raise NotImplementedError(f"No parquet reader for type {type(data)} registered.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@singledispatch
|
|
82
|
+
def write_json(data: object, filepath: str, name: str) -> None:
|
|
83
|
+
"""Writes data to a json file."""
|
|
84
|
+
raise NotImplementedError(f"No json writer for type {type(data)} registered.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@singledispatch
|
|
88
|
+
def read_json(data: object, filepath: str) -> Any:
|
|
89
|
+
"""Reads from a json file"""
|
|
90
|
+
raise NotImplementedError(f"No json reader for type {type(data)} registered.")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@singledispatch
|
|
94
|
+
def write_pickle(data: Any, filepath: str, name: str) -> None:
|
|
95
|
+
"""Writes data to a pickle file."""
|
|
96
|
+
raise NotImplementedError(f"No object writer for type {type(data)} registered.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@singledispatch
|
|
100
|
+
def read_pickle(data: Any, filtepath: str) -> object:
|
|
101
|
+
"""Reads from a pickle file"""
|
|
102
|
+
raise NotImplementedError(f"No object reader for type {type(data)} registered.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
import pandas as pd # conditional import to avoid pandas dependency
|
|
107
|
+
|
|
108
|
+
@write_json.register(pd.DataFrame)
|
|
109
|
+
def write_json_pd1(data: pd.DataFrame, filepath: str, name: str) -> None:
|
|
110
|
+
"""Writes a dataframe to a feather file."""
|
|
111
|
+
return data.to_json(filepath)
|
|
112
|
+
|
|
113
|
+
@write_json.register(pd.Series)
|
|
114
|
+
def write_json_pd2(data: pd.Series, filepath: str, name: str) -> None:
|
|
115
|
+
"""Writes a series to a feather file."""
|
|
116
|
+
_df = data.to_frame(name=name)
|
|
117
|
+
return _df.to_json(filepath)
|
|
118
|
+
|
|
119
|
+
@read_json.register(pd.Series)
|
|
120
|
+
def read_json_pd1(data: pd.Series, filepath: str) -> pd.Series:
|
|
121
|
+
"""Reads a series from a feather file."""
|
|
122
|
+
_df = pd.read_json(filepath)
|
|
123
|
+
return _df[_df.columns[0]]
|
|
124
|
+
|
|
125
|
+
@read_json.register(pd.DataFrame)
|
|
126
|
+
def read_json_pd2(data: pd.DataFrame, filepath: str) -> pd.DataFrame:
|
|
127
|
+
"""Reads a dataframe from a feather file."""
|
|
128
|
+
return pd.read_json(filepath)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
import pyarrow # noqa: F401 # conditional import to avoid pyarrow dependency
|
|
132
|
+
|
|
133
|
+
@write_feather.register(pd.DataFrame)
|
|
134
|
+
def write_feather_pd1(data: pd.DataFrame, filepath: str, name: str) -> None:
|
|
135
|
+
"""Writes a dataframe to a feather file."""
|
|
136
|
+
data.to_feather(filepath)
|
|
137
|
+
|
|
138
|
+
@write_feather.register(pd.Series)
|
|
139
|
+
def write_feather_pd2(data: pd.Series, filepath: str, name: str) -> None:
|
|
140
|
+
"""Writes a series to a feather file."""
|
|
141
|
+
data.to_frame(name=name).to_feather(filepath)
|
|
142
|
+
|
|
143
|
+
@read_feather.register(pd.DataFrame)
|
|
144
|
+
def read_feather_pd1(data: pd.DataFrame, filepath: str) -> pd.DataFrame:
|
|
145
|
+
"""Reads a dataframe from a feather file."""
|
|
146
|
+
return pd.read_feather(filepath)
|
|
147
|
+
|
|
148
|
+
@read_feather.register(pd.Series)
|
|
149
|
+
def read_feather_pd2(data: pd.Series, filepath: str) -> pd.Series:
|
|
150
|
+
"""Reads a series from a feather file."""
|
|
151
|
+
_df = pd.read_feather(filepath)
|
|
152
|
+
return _df[_df.columns[0]]
|
|
153
|
+
|
|
154
|
+
@write_parquet.register(pd.DataFrame)
|
|
155
|
+
def write_parquet_pd1(data: pd.DataFrame, filepath: str, name: str) -> None:
|
|
156
|
+
"""Writes a dataframe to a parquet file."""
|
|
157
|
+
data.to_parquet(filepath)
|
|
158
|
+
|
|
159
|
+
@write_parquet.register(pd.Series)
|
|
160
|
+
def write_parquet_pd2(data: pd.Series, filepath: str, name: str) -> None:
|
|
161
|
+
"""Writes a data frame to a parquet file."""
|
|
162
|
+
data.to_frame(name=name).to_parquet(filepath)
|
|
163
|
+
|
|
164
|
+
@read_parquet.register(pd.DataFrame)
|
|
165
|
+
def read_parquet_pd1(data: pd.DataFrame, filepath: str) -> pd.DataFrame:
|
|
166
|
+
"""Reads a dataframe from a parquet file."""
|
|
167
|
+
return pd.read_parquet(filepath)
|
|
168
|
+
|
|
169
|
+
@read_parquet.register(pd.Series)
|
|
170
|
+
def read_parquet_pd2(data: pd.Series, filepath: str) -> pd.Series:
|
|
171
|
+
"""Reads a series from a parquet file."""
|
|
172
|
+
_df = pd.read_parquet(filepath)
|
|
173
|
+
return _df[_df.columns[0]]
|
|
174
|
+
|
|
175
|
+
except ImportError:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
except ImportError:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@write_json.register(dict)
|
|
184
|
+
def write_json_dict(data: dict, filepath: str, name: str) -> None:
|
|
185
|
+
"""Writes a dictionary to a JSON file."""
|
|
186
|
+
if isinstance(data, dict):
|
|
187
|
+
with open(filepath, "w", encoding="utf8") as file:
|
|
188
|
+
json.dump(data, file)
|
|
189
|
+
else:
|
|
190
|
+
raise ValueError(f"Expected a dict, got {type(data)}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@read_json.register(dict)
|
|
194
|
+
def read_json_dict(data: dict, filepath: str) -> dict:
|
|
195
|
+
"""Reads a dictionary from a JSON file."""
|
|
196
|
+
with open(filepath, "r", encoding="utf8") as file:
|
|
197
|
+
return json.load(file)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@write_pickle.register(object)
|
|
201
|
+
def write_pickle_object(data: object, filepath: str, name: str) -> None:
|
|
202
|
+
if isinstance(data, object):
|
|
203
|
+
with open(filepath, "wb") as file:
|
|
204
|
+
pickle.dump(data, file)
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError(f"Expected an object, got {type(data)}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@read_pickle.register(object)
|
|
210
|
+
def read_pickle_object(data: object, filepath: str) -> object:
|
|
211
|
+
"""Reads a pickle file"""
|
|
212
|
+
with open(filepath, "rb") as file:
|
|
213
|
+
return pickle.load(file)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class CachingGraphAdapter(SimplePythonGraphAdapter):
|
|
217
|
+
"""Caching adapter.
|
|
218
|
+
|
|
219
|
+
Any node with tag "cache" will be cached (or loaded from cache) in the format defined by the
|
|
220
|
+
tag's value. There are a handful of formats supported, and other formats' readers and writers
|
|
221
|
+
can be provided to the constructor.
|
|
222
|
+
|
|
223
|
+
Values are loaded from cache if the node's file exists, unless one of these is true:
|
|
224
|
+
* node is explicitly forced to be computed with a constructor argument,
|
|
225
|
+
* any of its (potentially transitive) dependencies that are configured to be cached
|
|
226
|
+
was nevertheless computed (either forced or missing cached file).
|
|
227
|
+
|
|
228
|
+
Custom Serializers
|
|
229
|
+
------------------
|
|
230
|
+
|
|
231
|
+
One can provide custom readers and writers for any format by passing them to the constructor.
|
|
232
|
+
These readers and writers will override the default ones. If you don't want to override, but
|
|
233
|
+
rather extend the default ones, you can do so by registering them with the `register` method
|
|
234
|
+
on the appropriate function.
|
|
235
|
+
|
|
236
|
+
Writer functions need to have the following signature:
|
|
237
|
+
`def write_<format>(data: Any, filepath: str, name: str) -> None: ...`
|
|
238
|
+
where `data` is the data to be written, `filepath` is the path to the file to be written to,
|
|
239
|
+
and `name` is the name of the node that is being written.
|
|
240
|
+
|
|
241
|
+
Reader functions need to have the following signature:
|
|
242
|
+
`def read_<format>(data: Any, filepath: str) -> Any: ...`
|
|
243
|
+
where `data` is an EMPTY OBJECT of the type you wish to instantiate, and `filepath` is the
|
|
244
|
+
path to the file to be read from.
|
|
245
|
+
|
|
246
|
+
For example, if you want to extend JSON reader/writer to work with your custom type `T`,
|
|
247
|
+
you can do the following:
|
|
248
|
+
|
|
249
|
+
.. code-block:: python
|
|
250
|
+
|
|
251
|
+
@write_json.register(T)
|
|
252
|
+
def write_json_pd1(data: T, filepath: str, name: str) -> None: ...
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@read_json.register(T)
|
|
256
|
+
def read_json_dict(data: T, filepath: str) -> T: ...
|
|
257
|
+
|
|
258
|
+
Usage
|
|
259
|
+
-----
|
|
260
|
+
|
|
261
|
+
This is a simple example of the usage of `CachingGraphAdapter`.
|
|
262
|
+
|
|
263
|
+
First, let's define some nodes in `nodes.py`:
|
|
264
|
+
|
|
265
|
+
.. code-block:: python
|
|
266
|
+
|
|
267
|
+
import pandas as pd
|
|
268
|
+
from hamilton.function_modifiers import tag
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def data_a() -> pd.DataFrame: ...
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@tag(cache="parquet")
|
|
275
|
+
def data_b() -> pd.DataFrame: ...
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def transformed(data_a: pd.DataFrame, data_b: pd.DataFrame) -> pd.DataFrame: ...
|
|
279
|
+
|
|
280
|
+
Notice that `data_b` is configured to be cached in a parquet file.
|
|
281
|
+
|
|
282
|
+
We then simply initialize the driver with a caching adapter:
|
|
283
|
+
|
|
284
|
+
.. code-block:: python
|
|
285
|
+
|
|
286
|
+
from hamilton import base
|
|
287
|
+
from hamilton.driver import Driver
|
|
288
|
+
from hamilton.experimental import h_cache
|
|
289
|
+
|
|
290
|
+
import nodes
|
|
291
|
+
|
|
292
|
+
adapter = h_cache.CachingGraphAdapter(cache_path, base.PandasDataFrameResult())
|
|
293
|
+
dr = Driver(config, nodes, adapter=adapter)
|
|
294
|
+
result = dr.execute(["transformed"])
|
|
295
|
+
|
|
296
|
+
# Because `data_b` has been cached now, only `data_a` and `transformed` nodes
|
|
297
|
+
# will actually run.
|
|
298
|
+
result = dr.execute(["transformed"])
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(
|
|
302
|
+
self,
|
|
303
|
+
cache_path: str,
|
|
304
|
+
*args,
|
|
305
|
+
force_compute: set[str] | None = None,
|
|
306
|
+
writers: dict[str, Callable[[Any, str, str], None]] | None = None,
|
|
307
|
+
readers: dict[str, Callable[[Any, str], Any]] | None = None,
|
|
308
|
+
**kwargs,
|
|
309
|
+
):
|
|
310
|
+
"""Constructs the adapter.
|
|
311
|
+
|
|
312
|
+
:param cache_path: Path to the directory where cached files are stored.
|
|
313
|
+
:param force_compute: Set of nodes that should be forced to compute even if cache exists.
|
|
314
|
+
:param writers: A dictionary of writers for custom formats.
|
|
315
|
+
:param readers: A dictionary of readers for custom formats.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
super().__init__(*args, **kwargs)
|
|
319
|
+
self.cache_path = cache_path
|
|
320
|
+
self.force_compute = force_compute if force_compute is not None else {}
|
|
321
|
+
self.computed_nodes = set()
|
|
322
|
+
|
|
323
|
+
self.writers = writers or {}
|
|
324
|
+
self.readers = readers or {}
|
|
325
|
+
|
|
326
|
+
self._init_default_readers_writers()
|
|
327
|
+
|
|
328
|
+
def _init_default_readers_writers(self):
|
|
329
|
+
if "json" not in self.writers:
|
|
330
|
+
self.writers["json"] = write_json
|
|
331
|
+
if "json" not in self.readers:
|
|
332
|
+
self.readers["json"] = read_json
|
|
333
|
+
|
|
334
|
+
if "feather" not in self.writers:
|
|
335
|
+
self.writers["feather"] = write_feather
|
|
336
|
+
if "feather" not in self.readers:
|
|
337
|
+
self.readers["feather"] = read_feather
|
|
338
|
+
|
|
339
|
+
if "parquet" not in self.writers:
|
|
340
|
+
self.writers["parquet"] = write_parquet
|
|
341
|
+
if "parquet" not in self.readers:
|
|
342
|
+
self.readers["parquet"] = read_parquet
|
|
343
|
+
|
|
344
|
+
if "pickle" not in self.writers:
|
|
345
|
+
self.writers["pickle"] = write_pickle
|
|
346
|
+
if "pickle" not in self.readers:
|
|
347
|
+
self.readers["pickle"] = read_pickle
|
|
348
|
+
|
|
349
|
+
def _check_format(self, fmt):
|
|
350
|
+
if fmt not in self.writers:
|
|
351
|
+
raise ValueError(f"invalid cache format: {fmt}")
|
|
352
|
+
|
|
353
|
+
def _write_cache(self, fmt: str, data: Any, filepath: str, node_name: str) -> None:
|
|
354
|
+
self._check_format(fmt)
|
|
355
|
+
self.writers[fmt](data, filepath, node_name)
|
|
356
|
+
|
|
357
|
+
def _read_cache(self, fmt: str, expected_type: Any, filepath: str) -> None:
|
|
358
|
+
self._check_format(fmt)
|
|
359
|
+
return self.readers[fmt](expected_type, filepath)
|
|
360
|
+
|
|
361
|
+
def _get_empty_expected_type(self, expected_type: type) -> Any:
|
|
362
|
+
if typing_inspect.is_generic_type(expected_type):
|
|
363
|
+
return typing_inspect.get_origin(expected_type)()
|
|
364
|
+
return expected_type() # This ASSUMES that we can just do `str()`, `pd.DataFrame()`, etc.
|
|
365
|
+
|
|
366
|
+
def execute_node(self, node: Node, kwargs: dict[str, Any]) -> Any:
|
|
367
|
+
"""Executes nodes conditionally according to caching rules.
|
|
368
|
+
|
|
369
|
+
This node is executed if at least one of these is true:
|
|
370
|
+
|
|
371
|
+
* no cache is present,
|
|
372
|
+
* it is explicitly forced by passing it to the adapter in ``force_compute``,
|
|
373
|
+
* at least one of its upstream nodes that had a @cache annotation was computed,
|
|
374
|
+
either due to lack of cache or being explicitly forced.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
cache_format = node.tags.get("cache")
|
|
378
|
+
implicitly_forced = any(dep.name in self.computed_nodes for dep in node.dependencies)
|
|
379
|
+
if cache_format is not None:
|
|
380
|
+
filepath = f"{self.cache_path}/{node.name}.{cache_format}"
|
|
381
|
+
explicitly_forced = node.name in self.force_compute
|
|
382
|
+
if explicitly_forced or implicitly_forced or not os.path.exists(filepath):
|
|
383
|
+
result = node.callable(**kwargs)
|
|
384
|
+
logger.debug(
|
|
385
|
+
"Writing cache for %s to %s with type %s to %s",
|
|
386
|
+
node.name,
|
|
387
|
+
filepath,
|
|
388
|
+
type(result),
|
|
389
|
+
cache_format,
|
|
390
|
+
)
|
|
391
|
+
self._write_cache(cache_format, result, filepath, node.name)
|
|
392
|
+
self.computed_nodes.add(node.name)
|
|
393
|
+
return result
|
|
394
|
+
empty_expected_type = self._get_empty_expected_type(node.type)
|
|
395
|
+
logger.debug(
|
|
396
|
+
"Reading cache for %s from %s with type %s to %s",
|
|
397
|
+
node.name,
|
|
398
|
+
filepath,
|
|
399
|
+
type(empty_expected_type),
|
|
400
|
+
cache_format,
|
|
401
|
+
)
|
|
402
|
+
return self._read_cache(cache_format, empty_expected_type, filepath)
|
|
403
|
+
|
|
404
|
+
if implicitly_forced:
|
|
405
|
+
# For purposes of caching, we only mark it as computed if any cached input was computed.
|
|
406
|
+
# Otherwise, dependants would always be recomputed if they have a non-cached dependency.
|
|
407
|
+
self.computed_nodes.add(node.name)
|
|
408
|
+
return node.callable(**kwargs)
|
|
409
|
+
|
|
410
|
+
def build_result(self, **outputs: dict[str, Any]) -> Any:
|
|
411
|
+
"""Clears the computed nodes information and delegates to the super class."""
|
|
412
|
+
self.computed_nodes = set()
|
|
413
|
+
return super().build_result(**outputs)
|