Skip to content

Commit b1d0e68

Browse files
Dev-iLclaude
andcommitted
Standardize fingerprint hashing on xxh3_128
Swap the single `_hash_bytes` chokepoint from md5 to the non-cryptographic `xxhash.xxh3_128`. xxh3_128 produces a 16-byte digest (24 base64url chars, identical width to the md5 it replaces), so digest width and collision resistance are preserved while throughput on buffer-bound paths rises substantially. Declare `xxhash>=0.8.0` as a core runtime dependency (xxh3_128 was added in 0.8.0); fingerprinting is imported eagerly via the caching adapter, so it must be a hard dependency rather than an optional extra. Add the xxhash BSD-2-Clause attribution to LICENSE. Recompute the portable literal-digest pins (primitives, sequences, mappings, sets, numpy) against xxh3_128. This is a fingerprint-changing release: prior cached fingerprints no longer match and will be recomputed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 045a5db commit b1d0e68

4 files changed

Lines changed: 51 additions & 15 deletions

File tree

LICENSE

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,33 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
226226
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
227227
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
228228
SOFTWARE.
229+
230+
-------------------------------
231+
232+
This product depends on xxhash (the python-xxhash package, https://github.com/ifduyue/python-xxhash),
233+
which is licensed under the BSD 2-Clause License.
234+
235+
BSD 2-Clause License
236+
237+
Copyright (c) 2014-2024, Yue Du
238+
All rights reserved.
239+
240+
Redistribution and use in source and binary forms, with or without modification,
241+
are permitted provided that the following conditions are met:
242+
243+
* Redistributions of source code must retain the above copyright notice,
244+
this list of conditions and the following disclaimer.
245+
* Redistributions in binary form must reproduce the above copyright notice,
246+
this list of conditions and the following disclaimer in the documentation
247+
and/or other materials provided with the distribution.
248+
249+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
250+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
251+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
252+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
253+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
254+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
255+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
256+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
257+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
258+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

hamilton/caching/fingerprinting.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@
3636
import base64
3737
import datetime
3838
import functools
39-
import hashlib
4039
import logging
4140
import sys
4241
from collections.abc import Mapping, Sequence, Set
4342

43+
import xxhash
44+
4445
from hamilton.experimental import h_databackends
4546

4647
# NoneType is introduced in Python 3.10
@@ -77,12 +78,16 @@ def _compact_hash(digest: bytes) -> str:
7778

7879

7980
def _hash_bytes(data: bytes) -> str:
80-
"""Hash raw bytes and compact-encode the digest.
81+
"""Hash raw bytes with the non-cryptographic xxh3_128 algorithm and
82+
compact-encode the digest.
8183
8284
All hashing in this module routes through this single helper so the
8385
underlying hashing algorithm can be changed in exactly one place.
86+
xxh3_128 produces a 16-byte digest (24 base64url chars, the same width
87+
as the md5 it replaces) while running substantially faster on
88+
buffer-bound paths.
8489
"""
85-
return _compact_hash(hashlib.md5(data).digest())
90+
return _compact_hash(xxhash.xxh3_128(data).digest())
8691

8792

8893
@functools.singledispatch

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dependencies = [
5151
"pandas",
5252
"typing_extensions > 4.0.0",
5353
"typing_inspect",
54+
"xxhash>=0.8.0",
5455
]
5556

5657
[project.optional-dependencies]

tests/caching/test_fingerprinting.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ def __init__(self, obj):
146146
@pytest.mark.parametrize(
147147
("obj", "expected_hash"),
148148
[
149-
("hello-world", "L1Q1Kh6_t1atHO_H8RbBeA=="),
150-
(17.31231, "mJPTpPyXDSZgU-u8NuztIQ=="),
151-
(16474, "6MgAp1NbMW0ZZpe_8iKVsg=="),
152-
(True, "J2eGynSuIpd5bwVQzO9VVg=="),
153-
(b"\x951!\x89u=\xe6\xadG\xdf", "d1DufDgRQmqi9Kt4Z2PeUQ=="),
149+
("hello-world", "EXXR8_e47ElS18aP2lThJA=="),
150+
(17.31231, "tVUSIslYiBcW52c-7w4gvA=="),
151+
(16474, "FAJ-iXM_Hwg9TCRreY8AyA=="),
152+
(True, "qkJEg3-XQKmGWk5sWqmonw=="),
153+
(b"\x951!\x89u=\xe6\xadG\xdf", "pPTyYkSU_x7NLB1Fp_YTyA=="),
154154
],
155155
)
156156
def test_hash_primitive(obj, expected_hash):
@@ -161,8 +161,8 @@ def test_hash_primitive(obj, expected_hash):
161161
@pytest.mark.parametrize(
162162
("obj", "expected_hash"),
163163
[
164-
([0, True, "hello-world"], "mlOjj4yeCrSDFSn5zgdEIg=="),
165-
((17.0, False, "world"), "BcRSGfyKeIOdym9B6TmAyQ=="),
164+
([0, True, "hello-world"], "I98OkNhfxtScJrYNTs4ZfQ=="),
165+
((17.0, False, "world"), "catgOMSnsbQj1_KELNQscw=="),
166166
],
167167
)
168168
def test_hash_sequence(obj, expected_hash):
@@ -173,7 +173,7 @@ def test_hash_sequence(obj, expected_hash):
173173
def test_hash_equals_for_different_sequence_types():
174174
list_obj = [0, True, "hello-world"]
175175
tuple_obj = (0, True, "hello-world")
176-
expected_hash = "mlOjj4yeCrSDFSn5zgdEIg=="
176+
expected_hash = "I98OkNhfxtScJrYNTs4ZfQ=="
177177

178178
list_fingerprint = fingerprinting.hash_sequence(list_obj)
179179
tuple_fingerprint = fingerprinting.hash_sequence(tuple_obj)
@@ -182,7 +182,7 @@ def test_hash_equals_for_different_sequence_types():
182182

183183
def test_hash_ordered_mapping():
184184
obj = {0: True, "key": "value", 17.0: None}
185-
expected_hash = "GyxyI9-pq-EJJvSAIN509g=="
185+
expected_hash = "zX6MzhWGAOvxateHIPxOvA=="
186186
fingerprint = fingerprinting.hash_mapping(obj, ignore_order=False)
187187
assert fingerprint == expected_hash
188188

@@ -197,7 +197,7 @@ def test_hash_mapping_where_order_matters():
197197

198198
def test_hash_unordered_mapping():
199199
obj = {0: True, "key": "value", 17.0: None}
200-
expected_hash = "cDuuL2eA3DaSWlWW3u7o9g=="
200+
expected_hash = "4cnTFA4MEEzmBN4a04k6tA=="
201201
fingerprint = fingerprinting.hash_mapping(obj, ignore_order=True)
202202
assert fingerprint == expected_hash
203203

@@ -212,7 +212,7 @@ def test_hash_mapping_where_order_doesnt_matter():
212212

213213
def test_hash_set():
214214
obj = {0, True, "key", "value", 17.0, None}
215-
expected_hash = "E_f_tjbi6qn7KL3NUCZayg=="
215+
expected_hash = "mswHhNBBYN5mv6i-LcEeVw=="
216216
fingerprint = fingerprinting.hash_set(obj)
217217
assert fingerprint == expected_hash
218218

@@ -221,7 +221,7 @@ def test_hash_numpy():
221221
# dtype is pinned explicitly so the literal digest is reproducible across
222222
# platforms (the default integer dtype is platform-dependent).
223223
array = np.array([[0, 1], [2, 3]], dtype=np.int64)
224-
expected_hash = "024zwZIcWy6r4dlX4AMTow=="
224+
expected_hash = "Y1uek_eQTHejo2YtRvdWPQ=="
225225
fingerprint = fingerprinting.hash_value(array)
226226
assert fingerprint == expected_hash
227227

0 commit comments

Comments
 (0)