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,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)