Skip to content

feat: Improving checksum generation robustness and usability - #18

Open
manasaV3 wants to merge 5 commits into
mainfrom
mvenkatakrishnan/checksum_refactor
Open

feat: Improving checksum generation robustness and usability#18
manasaV3 wants to merge 5 commits into
mainfrom
mvenkatakrishnan/checksum_refactor

Conversation

@manasaV3

@manasaV3 manasaV3 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors checksum generation to be reliable and efficient when no algorithm is explicitly provided.

  • S3 files: Detects the stored checksum algorithm (from native S3 checksums or user metadata) and reuses the stored value instead of recomputing
  • S3 folders: Finds the best algorithm shared by all children via set intersection (computed checksums preferred over native), falling back to blake3 if no common algorithm exists
  • Cache-driven compute: Checksums fetched during detection are cached and threaded through _hash_s3_prefix_hash_s3_file, eliminating redundant S3 API calls during the compute phase
  • Early exit: Folder detection stops at the first file with no checksums or an algorithm mismatch, avoiding unnecessary API calls for large mixed-algorithm folders
  • Flag scoping: compute_if_no_s3_checksum=False now correctly skips only S3 assets; non-S3 platforms (HPC, coreweave, local) always proceed to computation
  • s3a:// support: URI parsing and platform detection now handle both s3:// and s3a:// schemes consistently
  • Test coverage: Rewrote test suite with parametrized tests, no redundant cases, and coverage for all new detection and caching behaviors

Comment thread dataset-catalog-client/catalog_client/utils/checksum/algorithms.py Fixed
Comment thread dataset-catalog-client/catalog_client/utils/checksum/algorithms.py Fixed
Comment thread dataset-catalog-client/catalog_client/utils/checksum/algorithms.py Fixed
@manasaV3
manasaV3 force-pushed the mvenkatakrishnan/checksum_refactor branch 2 times, most recently from f9b19cf to eb1d736 Compare April 27, 2026 09:31
@manasaV3 manasaV3 changed the title chore: checksum refactor feat: Improving checksum generation robustness and usability Apr 29, 2026
@manasaV3
manasaV3 force-pushed the mvenkatakrishnan/checksum_refactor branch from a266ff1 to c40f1ce Compare May 7, 2026 02:31
Compute a checksum for a local path or S3 URI (s3:// or s3a://).
Delegates to compute_checksum_s3 or compute_checksum_localfs.
"""
if path.startswith(("s3://", "s3a://")):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, do we have s3a:// data location? It looks like the checksum supports only s3:// by checking the storage_platform.

@manasaV3 manasaV3 Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't right now. It was more a of an edge case that was pre-emptively handled. :)

Replace catalog_client/utils/checksums.py with a catalog_client/utils/checksum
package split by concern: algorithm (hasher construction), hashing (streaming
and Merkle combination), s3 (stored-checksum discovery), generate (asset and
location entry points), and models.

- Adds crc64 (ECMA-182) and crc64nvme alongside blake3, blake2b, and crc32
- Supports AssetType.folder via Merkle-root combination of per-file digests
- Reuses checksums S3 already stores instead of downloading when possible
- Moves the optional hashing dependencies into a "checksum" extra and imports
  them lazily, so importing catalog_client never requires them
- Adds docs/checksum_guide.md and updates the USAGE.md checksum section

Note: the alpha checksum API changes shape. generate_for_assets is now
for_assets, and get_supported_algorithms is replaced by the Algorithm enum.
Both are re-exported from catalog_client.utils alongside the manifest helpers.
@manasaV3
manasaV3 force-pushed the mvenkatakrishnan/checksum_refactor branch from 12d55c8 to 4943680 Compare August 8, 2026 05:07
mvenkatakrishnan and others added 4 commits August 8, 2026 04:57
The added `uv sync --group dev --extra checksum` step was undone by the
following `uv run --group dev pytest`, which re-resolves the environment
without `--extra checksum` and prunes blake3, crcmod and awscrt back out.

Collapse both into a single `uv run --group dev --extra checksum pytest`
so the extras are present for the tests that need them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide described algorithm auto-detection as a two-tier order that
preferred native S3 checksums over user metadata. The implementation is a
single `max()` over `ALGORITHM_PRIORITY`, where blake3 (100) outranks the
native crc32 (60) and crc64nvme (70) — so the documented order was
inverted. Restate the real ranking, point at the source constant, and
document the folder rule (strongest algorithm common to every child).

Other corrections:

- crc64 was listed as a native S3 checksum field; `_S3_NATIVE_RESPONSE_KEY`
  contains only crc32 and crc64nvme.
- crc32 was credited to `binascii`; `_CRC32Hasher` uses `zlib`.
- The local-filesystem example used `StoragePlatform.local`, which is not a
  member of the enum and raised AttributeError. Use `sf_hpc` and add the
  imports the snippet was missing.
- The S3 `for_location` example printed None, because `for_location`
  defaults `compute_if_no_s3_checksum=False` while `for_assets` defaults
  True. Fix the example and document the asymmetry.
- The caching example passed a literal `...`, omitted the required
  `asset_type`, and imported ChecksumResult from the internal models module
  rather than the package root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_hash_stream accumulated each 256MB chunk in an io.BytesIO and then called
buf.getvalue(), which returns a second full copy — so peak memory scaled
with CHUNK_SIZE and every byte was copied twice before being hashed.

Keep a second live hasher for the current chunk and feed each 64KB read to
both hashers directly. Peak memory drops from ~512MB to one read buffer
regardless of CHUNK_SIZE. The flush condition is unchanged, so chunk
boundaries land in exactly the same byte positions.

Output is bit-identical: verified across 8 file sizes x 5 algorithms,
comparing file_hash, merkle_root and the full chunk manifest (index,
offset, size, hash) against the previous implementation, including the
case where reads do not divide evenly into chunks.

Cleanup carried in the same commit, since the source and test changes are
interdependent:

- Delete dead code: CRC_ALGORITHMS, CRYPTO_ALGORITHMS, and the _Hasher.raw()
  protocol method with its three implementations, none of which had call
  sites.
- Move _raw_from_hex to algorithm.py as raw_from_hex, so per-algorithm
  digest widths are declared once beside the hashers rather than in two
  places.
- Extract _directory_result() from the byte-identical block duplicated
  between _hash_local_dir and the S3 hash_tree closure.
- Move the S3 key-tree builder from hashing.py to s3.py as _insert_key; it
  was S3-specific logic in the backend-agnostic module, and the only
  module-level function without an underscore prefix.
- Import boto3 lazily in for_assets: importing catalog_client no longer
  pulls in boto3 and botocore (531 -> 311 modules).
- Replace a string literal sitting mid-branch in new_hasher (a no-op, not a
  docstring) with a comment, and compare Algorithm members rather than bare
  strings.
- Correct the module docstring, which listed boto3 as optional although it
  is a required dependency.

Tests:

- Replace three copy-pasted _setup_bucket methods with one s3 fixture that
  owns the mock_aws context, since a @mock_aws class decorator wraps only
  the test method and would leave fixture setup outside the mock.
- Use pytest.warns and tmp_path in place of hand-rolled
  warnings.catch_warnings and NamedTemporaryFile/os.unlink boilerplate,
  preserving the strict single-warning assertions.
- Delete one fully subsumed test, parametrize two pairs that differed only
  by an argument, drop never-passed helper parameters, and replace a
  positional call_args index with assert_called_once_with.
- Add tests/utils/checksum/__init__.py to match every other test package
  and avoid test module basename collisions.
- Record why the moto-backed suite and the mocked suite both exist: the
  mocked suite patches out the code the moto suite exercises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A digest now depends only on content, never on where that content lives or
how it was reached. A file hashes to the same value standalone as it does
inside a folder, and a checksum read from S3 is comparable with one computed
by downloading the object.

The two roles a digest plays -- what a location reports, and what a child
contributes to its parent -- were reading different fields (file_hash vs
merkle_root), so the same folder produced different roots depending on
whether its children had stored checksums. Both now read
ChecksumResult.content_digest.

Also in service of that property:

- Route folder assets by asset_type instead of a trailing slash, and
  normalise prefixes so "s3://b/ds" neither fails with NoSuchKey nor sweeps
  in "s3://b/ds2/".
- Ignore S3 multipart composite checksums (ChecksumType COMPOSITE, or a
  "-N" suffix): they cover part checksums, not object bytes, so their value
  depends on the uploader's part size.
- Reject stored values that are not well-formed digests for their algorithm,
  per-algorithm, so one bad value cannot hide an object's good ones.
- Fix crc64 to be the CRC-64/ECMA-182 it documents. crcmod's predefined
  "crc-64" is a different variant (check 0x46A5A9388A5BEFFE rather than
  0x6C40DF5F0B497347). Caught by a new known-vector test.

Behaviour and API changes:

- Stop swallowing every HeadObject error. Only a missing object means "no
  stored checksum"; access, credential and throttling errors propagate
  instead of silently forcing a download or a skip.
- Reuse cached folder children under compute_if_no_s3_checksum=False when
  the algorithm was auto-detected, matching the explicit-algorithm path.
- default_algorithm() returns blake3 when installed and blake2b otherwise,
  so a base install without the checksum extra produces checksums rather
  than warning on every asset.
- for_assets returns copies via model_copy() and no longer mutates caller
  assets; it is generic so DataAssetResponse survives the round trip.
- Every skip reports through ChecksumWarning, so one warnings filter catches
  all of them.
- Align compute_if_no_s3_checksum=True across for_location and for_assets.
- Drop the generic for_assets / for_location / compute_checksum re-exports
  from catalog_client.utils; import them from catalog_client.utils.checksum.

Removes catalog_client.utils.checksums with no shim. Shipped as a feature
release, not a major bump: the checksum API is alpha and documented as
subject to change. docs/checksum_guide.md carries a migration table.

Adds 115 tests, including a reproducibility suite that pins the property
above across four algorithms, and aligns the pre-commit mypy hook with the
mypy pin in pyproject.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>


class _Hasher(Protocol):
def update(self, data: bytes) -> None: ...
class _Hasher(Protocol):
def update(self, data: bytes) -> None: ...

def hexdigest(self) -> str: ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants