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,70 @@
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 base64
19
+ import zlib
20
+ from collections.abc import Mapping
21
+
22
+
23
+ def _compress_string(string: str) -> str:
24
+ return base64.b64encode(zlib.compress(string.encode(), level=3)).decode()
25
+
26
+
27
+ def _decompress_string(string: str) -> str:
28
+ return zlib.decompress(base64.b64decode(string.encode())).decode()
29
+
30
+
31
+ def _encode_str_dict(d: Mapping) -> str:
32
+ interleaved_tuple = tuple(item for pair in sorted(d.items()) for item in pair)
33
+ return ",".join(interleaved_tuple)
34
+
35
+
36
+ def _decode_str_dict(s: str) -> Mapping:
37
+ interleaved_tuple = tuple(s.split(","))
38
+ d = {}
39
+ for i in range(0, len(interleaved_tuple), 2):
40
+ d[interleaved_tuple[i]] = interleaved_tuple[i + 1]
41
+ return d
42
+
43
+
44
+ def decode_key(cache_key: str) -> dict:
45
+ node_name, _, code_and_data_string = cache_key.partition("-")
46
+ code_version, _, dep_encoded = code_and_data_string.partition("-")
47
+ data_stringified = _decompress_string(dep_encoded)
48
+
49
+ if data_stringified == "<none>":
50
+ dependencies_data_versions = {}
51
+ else:
52
+ dependencies_data_versions = _decode_str_dict(data_stringified)
53
+ return dict(
54
+ node_name=node_name,
55
+ code_version=code_version,
56
+ dependencies_data_versions=dependencies_data_versions,
57
+ )
58
+
59
+
60
+ def create_cache_key(
61
+ node_name: str, code_version: str, dependencies_data_versions: dict[str, str]
62
+ ) -> str:
63
+ if len(dependencies_data_versions.keys()) > 0:
64
+ dependencies_stringified = _encode_str_dict(dependencies_data_versions)
65
+ else:
66
+ dependencies_stringified = "<none>"
67
+
68
+ safe_node_name = "".join(c for c in node_name if c.isalnum() or c == "_").rstrip()
69
+
70
+ return f"{safe_node_name}-{code_version}-{_compress_string(dependencies_stringified)}"
@@ -0,0 +1,287 @@
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
+ This module contains hashing functions for Python objects. It uses
20
+ functools.singledispatch to allow specialized implementations based on type.
21
+ Singledispatch automatically applies the most specific implementation
22
+
23
+ This module houses implementations for the Python standard library. Supporting
24
+ all types is considerable endeavor, so we'll add support as types are requested
25
+ by users.
26
+
27
+ Otherwise, 3rd party types can be supported via the `h_databackends` module.
28
+ This registers abstract types that can be checked without having to import the
29
+ 3rd party library. For instance, there are implementations for pandas.DataFrame
30
+ and polars.DataFrame despite these libraries not being imported here.
31
+
32
+ IMPORTANT all container types that make a recursive call to `hash_value` or a specific
33
+ implementation should pass the `depth` parameter to prevent `RecursionError`.
34
+ """
35
+
36
+ import base64
37
+ import datetime
38
+ import functools
39
+ import hashlib
40
+ import logging
41
+ import sys
42
+ from collections.abc import Mapping, Sequence, Set
43
+
44
+ from hamilton.experimental import h_databackends
45
+
46
+ # NoneType is introduced in Python 3.10
47
+ try:
48
+ from types import NoneType
49
+ except ImportError:
50
+ NoneType = type(None)
51
+
52
+
53
+ logger = logging.getLogger("hamilton.caching")
54
+
55
+
56
+ MAX_DEPTH = 6
57
+ UNHASHABLE = "<unhashable>"
58
+ NONE_HASH = "<none>"
59
+
60
+
61
+ def set_max_depth(depth: int) -> None:
62
+ """Set the maximum recursion depth for fingerprinting non-supported types.
63
+
64
+ :param depth: The maximum depth for fingerprinting.
65
+ """
66
+ global MAX_DEPTH
67
+ MAX_DEPTH = depth
68
+
69
+
70
+ def _compact_hash(digest: bytes) -> str:
71
+ """Compact the hash to a string that's safe to pass around.
72
+
73
+ NOTE this is particularly relevant for the Hamilton UI and
74
+ passing hashes/fingerprints through web services.
75
+ """
76
+ return base64.urlsafe_b64encode(digest).decode()
77
+
78
+
79
+ @functools.singledispatch
80
+ def hash_value(obj, *args, depth=0, **kwargs) -> str:
81
+ """Fingerprinting strategy that computes a hash of the
82
+ full Python object.
83
+
84
+ The default case hashes the `__dict__` attribute of the
85
+ object (recursive).
86
+ """
87
+ if depth > MAX_DEPTH:
88
+ return UNHASHABLE
89
+
90
+ # __dict__ attribute contains the instance attributes of the object.
91
+ # this is typically sufficient to define the object and its behavior, so it's a good target
92
+ # for a hash in the default case.
93
+ # Objects that return an empty dict should be skipped (very odd behavior, happens with pandas type)
94
+ if getattr(obj, "__dict__", {}) != {}:
95
+ return hash_value(obj.__dict__, depth=depth + 1)
96
+
97
+ # check if the object comes from a module part of the standard library
98
+ # if it's the case, hash it's __repr__(), which is a string representation of the object
99
+ # __repr__() from the standard library should be well-formed and offer a reliable basis
100
+ # for fingerprinting.
101
+ # for example, this will catch: pathlib.Path, enum.Enum, argparse.Namespace
102
+ elif getattr(obj, "__module__", False):
103
+ if obj.__module__.partition(".")[0] in sys.builtin_module_names:
104
+ return hash_repr(obj, depth=depth)
105
+
106
+ # cover the datetime module, which doesn't have a __module__ attribute
107
+ elif type(obj) in vars(datetime).values():
108
+ return hash_repr(obj, depth=depth)
109
+
110
+ return UNHASHABLE
111
+
112
+
113
+ @hash_value.register(NoneType)
114
+ def hash_none(obj, *args, **kwargs) -> str:
115
+ """Hash for None is <none>
116
+
117
+ Primitive type returns a hash and doesn't have to handle depth.
118
+ """
119
+ return NONE_HASH
120
+
121
+
122
+ def hash_repr(obj, *args, **kwargs) -> str:
123
+ """Use the built-in repr() to get a string representation of the object
124
+ and hash it.
125
+
126
+ While `.__repr__()` might not be implemented for all classes, the function
127
+ `repr()` will handle it, along with exceptions, to always return a value.
128
+
129
+ Primitive type returns a hash and doesn't have to handle depth.
130
+ """
131
+ return hash_primitive(repr(obj))
132
+
133
+
134
+ # we need to use explicit multiple registration because older Python
135
+ # versions don't support type annotations with Union types
136
+ @hash_value.register(str)
137
+ @hash_value.register(int)
138
+ @hash_value.register(float)
139
+ @hash_value.register(bool)
140
+ def hash_primitive(obj, *args, **kwargs) -> str:
141
+ """Convert the primitive to a string and hash it
142
+
143
+ Primitive type returns a hash and doesn't have to handle depth.
144
+ """
145
+ hash_object = hashlib.md5(str(obj).encode())
146
+ return _compact_hash(hash_object.digest())
147
+
148
+
149
+ @hash_value.register(bytes)
150
+ def hash_bytes(obj, *args, **kwargs) -> str:
151
+ """Convert the primitive to a string and hash it
152
+
153
+ Primitive type returns a hash and doesn't have to handle depth.
154
+ """
155
+ hash_object = hashlib.md5(obj)
156
+ return _compact_hash(hash_object.digest())
157
+
158
+
159
+ @hash_value.register(Sequence)
160
+ def hash_sequence(obj, *args, depth: int = 0, **kwargs) -> str:
161
+ """Hash each object of the sequence.
162
+
163
+ Orders matters for the hash since orders matters in a sequence.
164
+ """
165
+ hash_object = hashlib.sha224()
166
+ for elem in obj:
167
+ hash_object.update(hash_value(elem, depth=depth + 1).encode())
168
+
169
+ return _compact_hash(hash_object.digest())
170
+
171
+
172
+ def hash_unordered_mapping(obj, *args, depth: int = 0, **kwargs) -> str:
173
+ """
174
+
175
+ When hashing an unordered mapping, the two following dict have the same hash.
176
+
177
+ .. code-block:: python
178
+
179
+ foo = {"key": 3, "key2": 13}
180
+ bar = {"key2": 13, "key": 3}
181
+
182
+ hash_mapping(foo) == hash_mapping(bar)
183
+ """
184
+
185
+ hashed_mapping: dict[str, str] = {}
186
+ for key, value in obj.items():
187
+ hashed_mapping[hash_value(key, depth=depth + 1)] = hash_value(value, depth=depth + 1)
188
+
189
+ hash_object = hashlib.sha224()
190
+ for key, value in sorted(hashed_mapping.items()):
191
+ hash_object.update(key.encode())
192
+ hash_object.update(value.encode())
193
+
194
+ return _compact_hash(hash_object.digest())
195
+
196
+
197
+ @hash_value.register(Mapping)
198
+ def hash_mapping(obj, *, ignore_order: bool = True, depth: int = 0, **kwargs) -> str:
199
+ """Hash each key then its value.
200
+
201
+ The mapping is always sorted first because order shouldn't matter
202
+ in a mapping.
203
+
204
+ NOTE Since Python 3.7, dictionary store insertion order. However, this
205
+ function assumes that they key order doesn't matter to uniquely identify
206
+ the dictionary.
207
+
208
+ .. code-block:: python
209
+
210
+ foo = {"key": 3, "key2": 13}
211
+ bar = {"key2": 13, "key": 3}
212
+
213
+ hash_mapping(foo) == hash_mapping(bar)
214
+
215
+ """
216
+ if ignore_order:
217
+ # use the same depth because we're simply dispatching to another implementation
218
+ return hash_unordered_mapping(obj, depth=depth)
219
+
220
+ hash_object = hashlib.sha224()
221
+ for key, value in obj.items():
222
+ hash_object.update(hash_value(key, depth=depth + 1).encode())
223
+ hash_object.update(hash_value(value, depth=depth + 1).encode())
224
+
225
+ return _compact_hash(hash_object.digest())
226
+
227
+
228
+ @hash_value.register(Set)
229
+ def hash_set(obj, *args, depth: int = 0, **kwargs) -> str:
230
+ """Hash each element of the set, then sort hashes, and
231
+ create a hash of hashes.
232
+
233
+ For the same objects in the set, the hashes will be the
234
+ same.
235
+ """
236
+ hashes = [hash_value(elem, depth=depth + 1) for elem in obj]
237
+ sorted_hashes = sorted(hashes)
238
+
239
+ hash_object = hashlib.sha224()
240
+ for hash in sorted_hashes:
241
+ hash_object.update(hash.encode())
242
+
243
+ return _compact_hash(hash_object.digest())
244
+
245
+
246
+ @hash_value.register(h_databackends.AbstractPandasDataFrame)
247
+ @hash_value.register(h_databackends.AbstractPandasColumn)
248
+ def hash_pandas_obj(obj, *args, depth: int = 0, **kwargs) -> str:
249
+ """Convert a pandas dataframe, series, or index to
250
+ a dictionary of {index: row_hash} then hash it.
251
+
252
+ Given the hashing for mappings, the physical ordering or rows doesn't matter.
253
+ For example, if the index is a date, the hash will represent the {date: row_hash},
254
+ and won't preserve how dates were ordered in the DataFrame.
255
+ """
256
+ from pandas.util import hash_pandas_object
257
+
258
+ hash_per_row = hash_pandas_object(obj)
259
+ return hash_mapping(hash_per_row.to_dict(), ignore_order=False, depth=depth + 1)
260
+
261
+
262
+ @hash_value.register(h_databackends.AbstractPolarsDataFrame)
263
+ def hash_polars_dataframe(obj, *args, depth: int = 0, **kwargs) -> str:
264
+ """Convert a polars dataframe, series, or index to
265
+ a list of hashes then hash it.
266
+ """
267
+ hash_per_row = obj.hash_rows()
268
+ return hash_sequence(hash_per_row.to_list(), depth=depth + 1)
269
+
270
+
271
+ @hash_value.register(h_databackends.AbstractPolarsColumn)
272
+ def hash_polars_column(obj, *args, depth: int = 0, **kwargs) -> str:
273
+ """Promote the single Series to a dataframe and hash it"""
274
+ # use the same depth because we're simply dispatching to another implementation
275
+ return hash_polars_dataframe(obj.to_frame(), depth=depth)
276
+
277
+
278
+ @hash_value.register(h_databackends.AbstractNumpyArray)
279
+ def hash_numpy_array(obj, *args, depth: int = 0, **kwargs) -> str:
280
+ """Get the bytes representation of the array raw data and hash it.
281
+
282
+ Might not be ideal because different higher-level numpy objects could have
283
+ the same underlying array representation (e.g., masked arrays).
284
+ Unsure, but it's an area to investigate.
285
+ """
286
+ # use the same depth because we're simply dispatching to another implementation
287
+ return hash_bytes(obj.tobytes(), depth=depth)
@@ -0,0 +1,16 @@
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.
@@ -0,0 +1,242 @@
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 abc
19
+ import pickle
20
+ from collections.abc import Sequence
21
+ from datetime import datetime, timedelta, timezone
22
+ from typing import Any
23
+
24
+ from hamilton.htypes import custom_subclass_check
25
+ from hamilton.io.data_adapters import DataLoader, DataSaver
26
+ from hamilton.registry import LOADER_REGISTRY, SAVER_REGISTRY
27
+
28
+
29
+ class ResultRetrievalError(Exception):
30
+ """Raised by the SmartCacheAdapter when ResultStore.get() fails."""
31
+
32
+
33
+ # TODO Currently, this check is done when data needs to be saved.
34
+ # Ideally, it would be done earlier in the caching lifecycle.
35
+ def search_data_adapter_registry(
36
+ name: str, type_: type
37
+ ) -> tuple[type[DataSaver], type[DataLoader]]:
38
+ """Find pair of DataSaver and DataLoader registered with `name` and supporting `type_`"""
39
+ if name not in SAVER_REGISTRY or name not in LOADER_REGISTRY:
40
+ raise KeyError(
41
+ f"{name} isn't associated to both a DataLoader and a DataSaver. "
42
+ "Default saver/loader pairs include `json`, `file`, `pickle`, `parquet`, `csv`, "
43
+ "`feather`, `orc`, `excel`. More pairs may be available through plugins."
44
+ )
45
+
46
+ try:
47
+ saver_cls = next(
48
+ saver_cls
49
+ for saver_cls in SAVER_REGISTRY[name]
50
+ if any(
51
+ custom_subclass_check(type_, applicable_type)
52
+ for applicable_type in saver_cls.applicable_types()
53
+ )
54
+ )
55
+ except StopIteration as e:
56
+ raise KeyError(f"{name} doesn't have any DataSaver supporting type {type_}") from e
57
+
58
+ try:
59
+ loader_cls = next(
60
+ loader_cls
61
+ for loader_cls in LOADER_REGISTRY[name]
62
+ if any(
63
+ custom_subclass_check(type_, applicable_type)
64
+ for applicable_type in loader_cls.applicable_types()
65
+ )
66
+ )
67
+ except StopIteration as e:
68
+ raise KeyError(f"{name} doesn't have any DataLoader supporting type {type_}") from e
69
+
70
+ return saver_cls, loader_cls
71
+
72
+
73
+ class ResultStore(abc.ABC):
74
+ @abc.abstractmethod
75
+ def set(self, data_version: str, result: Any, **kwargs) -> None:
76
+ """Store ``result`` keyed by ``data_version``."""
77
+
78
+ @abc.abstractmethod
79
+ def get(self, data_version: str, **kwargs) -> Any | None:
80
+ """Try to retrieve ``result`` keyed by ``data_version``.
81
+ If retrieval misses, return ``None``.
82
+ """
83
+
84
+ @abc.abstractmethod
85
+ def delete(self, data_version: str) -> None:
86
+ """Delete ``result`` keyed by ``data_version``."""
87
+
88
+ @abc.abstractmethod
89
+ def delete_all(self) -> None:
90
+ """Delete all stored results."""
91
+
92
+ @abc.abstractmethod
93
+ def exists(self, data_version: str) -> bool:
94
+ """boolean check if a ``result`` is found for ``data_version``
95
+ If True, ``.get()`` should successfully retrieve the ``result``.
96
+ """
97
+
98
+
99
+ class MetadataStore(abc.ABC):
100
+ @abc.abstractmethod
101
+ def __len__(self) -> int:
102
+ """Return the number of cache_keys in the metadata store"""
103
+
104
+ @abc.abstractmethod
105
+ def initialize(self, run_id: str) -> None:
106
+ """Setup the metadata store and log the start of the run"""
107
+
108
+ @abc.abstractmethod
109
+ def set(self, cache_key: str, data_version: str, **kwargs) -> Any | None:
110
+ """Store the mapping ``cache_key -> data_version``.
111
+ Can include other metadata (e.g., node name, run id, code version) depending
112
+ on the implementation.
113
+ """
114
+
115
+ @abc.abstractmethod
116
+ def get(self, cache_key: str, **kwargs) -> str | None:
117
+ """Try to retrieve ``data_version`` keyed by ``cache_key``.
118
+ If retrieval misses return ``None``.
119
+ """
120
+
121
+ @abc.abstractmethod
122
+ def delete(self, cache_key: str) -> None:
123
+ """Delete ``data_version`` keyed by ``cache_key``."""
124
+
125
+ @abc.abstractmethod
126
+ def delete_all(self) -> None:
127
+ """Delete all stored metadata."""
128
+
129
+ @abc.abstractmethod
130
+ def exists(self, cache_key: str) -> bool:
131
+ """boolean check if a ``data_version`` is found for ``cache_key``
132
+ If True, ``.get()`` should successfully retrieve the ``data_version``.
133
+ """
134
+
135
+ @abc.abstractmethod
136
+ def get_run_ids(self) -> Sequence[str]:
137
+ """Return a list of run ids, sorted from oldest to newest start time.
138
+ A ``run_id`` is registered when the metadata_store ``.initialize()`` is called.
139
+ """
140
+
141
+ @abc.abstractmethod
142
+ def get_run(self, run_id: str) -> Sequence[dict]:
143
+ """Return a list of node metadata associated with a run.
144
+
145
+ For each node, the metadata should include ``cache_key`` (created or used)
146
+ and ``data_version``. These values allow to manually query the MetadataStore
147
+ or ResultStore.
148
+
149
+ Decoding the ``cache_key`` gives the ``node_name``, ``code_version``, and
150
+ ``dependencies_data_versions``. Individual implementations may add more
151
+ information or decode the ``cache_key`` before returning metadata.
152
+ """
153
+
154
+ @property
155
+ def size(self) -> int:
156
+ """Number of unique entries (i.e., cache_keys) in the metadata_store"""
157
+ return self.__len__()
158
+
159
+ @property
160
+ def last_run_id(self) -> str:
161
+ """Return"""
162
+ return self.get_run_ids()[-1]
163
+
164
+ def get_last_run(self) -> Any:
165
+ """Return the metadata from the last started run."""
166
+ return self.get_run(self.last_run_id)
167
+
168
+
169
+ # TODO refactor the association between StoredResult, MetadataStore, and ResultStore
170
+ # to load data using the `DataLoader` class and kwargs instead of pickling the instantiated
171
+ # DataLoader object. This would be safer across Hamilton versions.
172
+ class StoredResult:
173
+ def __init__(
174
+ self,
175
+ value: Any,
176
+ expires_at=None,
177
+ saver=None,
178
+ loader=None,
179
+ ):
180
+ self.value = value
181
+ self.expires_at = expires_at
182
+ self.saver = saver
183
+ self.loader = loader
184
+
185
+ @classmethod
186
+ def new(
187
+ cls,
188
+ value: Any,
189
+ expires_in: timedelta | None = None,
190
+ saver: DataSaver | None = None,
191
+ loader: DataLoader | None = None,
192
+ ) -> "StoredResult":
193
+ if expires_in is not None and not isinstance(expires_in, timedelta):
194
+ expires_in = timedelta(seconds=expires_in)
195
+
196
+ # != operator on boolean is XOR
197
+ if bool(saver is not None) != bool(loader is not None):
198
+ raise ValueError(
199
+ "Must pass both `saver` and `loader` or neither. Currently received: "
200
+ f"`saver`: `{saver}`; `loader`: `{loader}`"
201
+ )
202
+
203
+ return cls(
204
+ value=value,
205
+ expires_at=(datetime.now(tz=timezone.utc) + expires_in) if expires_in else None,
206
+ saver=saver,
207
+ loader=loader,
208
+ )
209
+
210
+ @property
211
+ def expired(self) -> bool:
212
+ return self.expires_at is not None and datetime.now(tz=timezone.utc) >= self.expires_at
213
+
214
+ @property
215
+ def expires_in(self) -> int:
216
+ if self.expires_at:
217
+ return int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
218
+
219
+ return -1
220
+
221
+ def save(self) -> bytes:
222
+ """Receives pickleable data or DataLoader to use to load the real data"""
223
+ if self.saver is not None:
224
+ self.saver.save_data(data=self.value)
225
+ to_pickle = self.loader
226
+ else:
227
+ to_pickle = self.value
228
+
229
+ return pickle.dumps(to_pickle)
230
+
231
+ @classmethod
232
+ def load(cls, raw: bytes) -> "StoredResult":
233
+ """Reads the raw bytes from disk and sets `StoredResult.data`"""
234
+ loaded = pickle.loads(raw)
235
+ if isinstance(loaded, DataLoader):
236
+ loader = loaded
237
+ result, metadata = loader.load_data(None)
238
+ else:
239
+ loader = None
240
+ result = loaded
241
+
242
+ return StoredResult.new(value=result)