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