Skip to content

cdm_data_loaders.utils.checksums

Algorithm-agnostic checksum helpers.

ChecksumEntry dataclass

A single expected checksum, tied to a specific algorithm.

Parameters:

Name Type Description Default
algorithm str

hashlib algorithm name the checksum was generated with

required
value str

expected digest, as lowercase hex

required
Source code in src/cdm_data_loaders/utils/checksums.py
@dataclass(frozen=True, slots=True)
class ChecksumEntry:
    """A single expected checksum, tied to a specific algorithm.

    :param algorithm: hashlib algorithm name the checksum was generated with
    :type algorithm: str
    :param value: expected digest, as lowercase hex
    :type value: str
    """

    algorithm: str
    value: str

Hasher

Bases: Protocol

Minimal interface satisfied by both hashlib hash objects and Crc64NvmeHasher.

Source code in src/cdm_data_loaders/utils/checksums.py
@runtime_checkable
class Hasher(Protocol):
    """Minimal interface satisfied by both hashlib hash objects and Crc64NvmeHasher."""

    def update(self, data: bytes) -> object:
        """Update the hash object with `data`, a bytes-like object.

        Repeated calls are equivalent to a single call with the concatenation of all the arguments: m.update(a); m.update(b) is equivalent to m.update(a+b).
        """
        ...

    def hexdigest(self) -> str:
        """Return the digest of the data passed to the update() method so far.

        The digest is returned as a string object of double length, containing only hexadecimal digits.

        :return: digest of data so far, as a string containing only hexadecimal digits
        :rtype: str
        """
        ...

update

update(data: bytes) -> object

Update the hash object with data, a bytes-like object.

Repeated calls are equivalent to a single call with the concatenation of all the arguments: m.update(a); m.update(b) is equivalent to m.update(a+b).

Source code in src/cdm_data_loaders/utils/checksums.py
def update(self, data: bytes) -> object:
    """Update the hash object with `data`, a bytes-like object.

    Repeated calls are equivalent to a single call with the concatenation of all the arguments: m.update(a); m.update(b) is equivalent to m.update(a+b).
    """
    ...

hexdigest

hexdigest() -> str

Return the digest of the data passed to the update() method so far.

The digest is returned as a string object of double length, containing only hexadecimal digits.

Returns:

Type Description
str

digest of data so far, as a string containing only hexadecimal digits

Source code in src/cdm_data_loaders/utils/checksums.py
def hexdigest(self) -> str:
    """Return the digest of the data passed to the update() method so far.

    The digest is returned as a string object of double length, containing only hexadecimal digits.

    :return: digest of data so far, as a string containing only hexadecimal digits
    :rtype: str
    """
    ...

Crc64NvmeHasher

Adapts awscrt's stateless crc64nvme(data, prev) function to a hashlib-like interface.

Source code in src/cdm_data_loaders/utils/checksums.py
class Crc64NvmeHasher:
    """Adapts awscrt's stateless crc64nvme(data, prev) function to a hashlib-like interface."""

    def __init__(self) -> None:
        """Create a new Crc64NvmeHasher with an initial CRC of 0."""
        self._crc = 0

    def update(self, data: bytes) -> None:
        """Update the hash object with `data`, a bytes-like object."""
        self._crc = _crc64nvme(data, self._crc)

    def digest(self) -> bytes:
        """Return the digest of the data passed to the update() method so far."""
        return self._crc.to_bytes(8, byteorder="big")

    def hexdigest(self) -> str:
        """Return the digest of the data passed to the update() method so far as a string of hexadecimal digits."""
        return self.digest().hex()

    def b64digest(self) -> str:
        """Base64 form, matching S3's ChecksumCRC64NVME representation."""
        return base64.b64encode(self.digest()).decode()

__init__

__init__() -> None

Create a new Crc64NvmeHasher with an initial CRC of 0.

Source code in src/cdm_data_loaders/utils/checksums.py
def __init__(self) -> None:
    """Create a new Crc64NvmeHasher with an initial CRC of 0."""
    self._crc = 0

update

update(data: bytes) -> None

Update the hash object with data, a bytes-like object.

Source code in src/cdm_data_loaders/utils/checksums.py
def update(self, data: bytes) -> None:
    """Update the hash object with `data`, a bytes-like object."""
    self._crc = _crc64nvme(data, self._crc)

digest

digest() -> bytes

Return the digest of the data passed to the update() method so far.

Source code in src/cdm_data_loaders/utils/checksums.py
def digest(self) -> bytes:
    """Return the digest of the data passed to the update() method so far."""
    return self._crc.to_bytes(8, byteorder="big")

hexdigest

hexdigest() -> str

Return the digest of the data passed to the update() method so far as a string of hexadecimal digits.

Source code in src/cdm_data_loaders/utils/checksums.py
def hexdigest(self) -> str:
    """Return the digest of the data passed to the update() method so far as a string of hexadecimal digits."""
    return self.digest().hex()

b64digest

b64digest() -> str

Base64 form, matching S3's ChecksumCRC64NVME representation.

Source code in src/cdm_data_loaders/utils/checksums.py
def b64digest(self) -> str:
    """Base64 form, matching S3's ChecksumCRC64NVME representation."""
    return base64.b64encode(self.digest()).decode()

HashingReader

Wraps a file-like object, computing a hash of everything read through it.

Transparently proxies attribute access (e.g. close) to the wrapped object so it remains compatible with s3transfer's fileobj introspection. Deliberately does not implement seek/tell itself, so non-seekable streams (such as requests' response.raw) are still treated as non-seekable by anything inspecting the wrapper.

Source code in src/cdm_data_loaders/utils/checksums.py
class HashingReader:
    """Wraps a file-like object, computing a hash of everything read through it.

    Transparently proxies attribute access (e.g. `close`) to the wrapped
    object so it remains compatible with s3transfer's fileobj introspection.
    Deliberately does not implement `seek`/`tell` itself, so non-seekable
    streams (such as `requests`' `response.raw`) are still treated as
    non-seekable by anything inspecting the wrapper.
    """

    def __init__(self, raw: Any, algorithm: str) -> None:
        """Initialise a hashing wrapper around a readable, file-like object.

        :param raw: the underlying file-like object to wrap; must implement `read`
        :type raw: Any
        :param algorithm: hashlib algorithm name to use for hashing, e.g. "md5"
        :type algorithm: str
        :raises ValueError: if `algorithm` is not a supported hashlib algorithm
        """
        self._raw = raw
        self._hash = new_hasher(algorithm)

    def read(self, amt: int = -1) -> bytes:
        """Read up to `amt` bytes from the wrapped object, updating the running hash.

        :param amt: number of bytes to read; -1 reads all remaining data, defaults to -1
        :type amt: int, optional
        :return: the bytes read
        :rtype: bytes
        """
        chunk = self._raw.read(amt)
        if chunk:
            self._hash.update(chunk)
        return chunk

    def __getattr__(self, item: str) -> Any:
        """Proxy any attribute not defined on this wrapper to the wrapped object.

        :param item: attribute name being accessed
        :type item: str
        :return: the attribute value from the wrapped object
        :rtype: Any
        :raises AttributeError: if `item` is one of this wrapper's own internal
            attributes (not yet set) or is missing from the wrapped object too
        """
        # __getattr__ is only invoked when normal attribute lookup fails, which
        # includes the case where `_raw` itself hasn't been assigned yet (e.g.
        # during copy/pickle/deepcopy, or attribute access before __init__ runs).
        # Without this guard, looking up `_raw` would recurse into this method
        # forever trying to resolve `self._raw`.
        if item in ("_raw", "_hash"):
            msg = f"{type(self).__name__!r} object has no attribute {item!r}"
            raise AttributeError(msg)
        return getattr(self._raw, item)

    def hexdigest(self) -> str:
        """Return the hex digest of all bytes read so far.

        :return: hex digest of the accumulated hash
        :rtype: str
        """
        return self._hash.hexdigest()

    def b64digest(self) -> str:
        """Return the base64 digest of all bytes read so far.

        Only supported for algorithms whose hasher implements `b64digest`
        (currently crc64nvme), matching S3's ChecksumCRC64NVME representation.

        :return: base64-encoded digest of the accumulated hash
        :rtype: str
        :raises AttributeError: if the underlying algorithm's hasher does not
            support base64 digests
        """
        return self._hash.b64digest()

__init__

__init__(raw: Any, algorithm: str) -> None

Initialise a hashing wrapper around a readable, file-like object.

Parameters:

Name Type Description Default
raw Any

the underlying file-like object to wrap; must implement read

required
algorithm str

hashlib algorithm name to use for hashing, e.g. "md5"

required

Raises:

Type Description
ValueError

if algorithm is not a supported hashlib algorithm

Source code in src/cdm_data_loaders/utils/checksums.py
def __init__(self, raw: Any, algorithm: str) -> None:
    """Initialise a hashing wrapper around a readable, file-like object.

    :param raw: the underlying file-like object to wrap; must implement `read`
    :type raw: Any
    :param algorithm: hashlib algorithm name to use for hashing, e.g. "md5"
    :type algorithm: str
    :raises ValueError: if `algorithm` is not a supported hashlib algorithm
    """
    self._raw = raw
    self._hash = new_hasher(algorithm)

read

read(amt: int = -1) -> bytes

Read up to amt bytes from the wrapped object, updating the running hash.

Parameters:

Name Type Description Default
amt int

number of bytes to read; -1 reads all remaining data, defaults to -1

-1

Returns:

Type Description
bytes

the bytes read

Source code in src/cdm_data_loaders/utils/checksums.py
def read(self, amt: int = -1) -> bytes:
    """Read up to `amt` bytes from the wrapped object, updating the running hash.

    :param amt: number of bytes to read; -1 reads all remaining data, defaults to -1
    :type amt: int, optional
    :return: the bytes read
    :rtype: bytes
    """
    chunk = self._raw.read(amt)
    if chunk:
        self._hash.update(chunk)
    return chunk

__getattr__

__getattr__(item: str) -> Any

Proxy any attribute not defined on this wrapper to the wrapped object.

Parameters:

Name Type Description Default
item str

attribute name being accessed

required

Returns:

Type Description
Any

the attribute value from the wrapped object

Raises:

Type Description
AttributeError

if item is one of this wrapper's own internal attributes (not yet set) or is missing from the wrapped object too

Source code in src/cdm_data_loaders/utils/checksums.py
def __getattr__(self, item: str) -> Any:
    """Proxy any attribute not defined on this wrapper to the wrapped object.

    :param item: attribute name being accessed
    :type item: str
    :return: the attribute value from the wrapped object
    :rtype: Any
    :raises AttributeError: if `item` is one of this wrapper's own internal
        attributes (not yet set) or is missing from the wrapped object too
    """
    # __getattr__ is only invoked when normal attribute lookup fails, which
    # includes the case where `_raw` itself hasn't been assigned yet (e.g.
    # during copy/pickle/deepcopy, or attribute access before __init__ runs).
    # Without this guard, looking up `_raw` would recurse into this method
    # forever trying to resolve `self._raw`.
    if item in ("_raw", "_hash"):
        msg = f"{type(self).__name__!r} object has no attribute {item!r}"
        raise AttributeError(msg)
    return getattr(self._raw, item)

hexdigest

hexdigest() -> str

Return the hex digest of all bytes read so far.

Returns:

Type Description
str

hex digest of the accumulated hash

Source code in src/cdm_data_loaders/utils/checksums.py
def hexdigest(self) -> str:
    """Return the hex digest of all bytes read so far.

    :return: hex digest of the accumulated hash
    :rtype: str
    """
    return self._hash.hexdigest()

b64digest

b64digest() -> str

Return the base64 digest of all bytes read so far.

Only supported for algorithms whose hasher implements b64digest (currently crc64nvme), matching S3's ChecksumCRC64NVME representation.

Returns:

Type Description
str

base64-encoded digest of the accumulated hash

Raises:

Type Description
AttributeError

if the underlying algorithm's hasher does not support base64 digests

Source code in src/cdm_data_loaders/utils/checksums.py
def b64digest(self) -> str:
    """Return the base64 digest of all bytes read so far.

    Only supported for algorithms whose hasher implements `b64digest`
    (currently crc64nvme), matching S3's ChecksumCRC64NVME representation.

    :return: base64-encoded digest of the accumulated hash
    :rtype: str
    :raises AttributeError: if the underlying algorithm's hasher does not
        support base64 digests
    """
    return self._hash.b64digest()

compute_stream_checksum

compute_stream_checksum(
    stream: IO[bytes],
    algorithm: str,
    chunk_size: int = HASH_CHUNK_BYTES,
) -> str

Compute a checksum from an open binary stream using any algorithm new_hasher supports.

Parameters:

Name Type Description Default
stream IO[bytes]

readable binary stream

required
algorithm str

hashlib algorithm name, or "crc64nvme"

required
chunk_size int

read chunk size in bytes

HASH_CHUNK_BYTES

Returns:

Type Description
str

hex digest, or base64 digest for crc64nvme (matching S3's format)

Source code in src/cdm_data_loaders/utils/checksums.py
def compute_stream_checksum(
    stream: IO[bytes],
    algorithm: str,
    chunk_size: int = HASH_CHUNK_BYTES,
) -> str:
    """Compute a checksum from an open binary stream using any algorithm ``new_hasher`` supports.

    :param stream: readable binary stream
    :type stream: IO[bytes]
    :param algorithm: hashlib algorithm name, or "crc64nvme"
    :type algorithm: str
    :param chunk_size: read chunk size in bytes
    :type chunk_size: int
    :return: hex digest, or base64 digest for crc64nvme (matching S3's format)
    :rtype: str
    """
    hasher: Hasher = new_hasher(algorithm)
    for chunk in iter(lambda: stream.read(chunk_size), b""):
        hasher.update(chunk)
    return _final_digest(hasher)

compute_file_checksum

compute_file_checksum(
    file_path: str | Path, algorithm: str
) -> str

Compute a checksum for a local file using any algorithm new_hasher supports.

Parameters:

Name Type Description Default
file_path str | Path

path to the file

required
algorithm str

hashlib algorithm name, or "crc64nvme"

required

Returns:

Type Description
str

hex digest, or base64 digest for crc64nvme (matching S3's format)

Source code in src/cdm_data_loaders/utils/checksums.py
def compute_file_checksum(file_path: str | Path, algorithm: str) -> str:
    """Compute a checksum for a local file using any algorithm new_hasher supports.

    :param file_path: path to the file
    :type file_path: str | Path
    :param algorithm: hashlib algorithm name, or "crc64nvme"
    :type algorithm: str
    :return: hex digest, or base64 digest for crc64nvme (matching S3's format)
    """
    with Path(file_path).open("rb") as f:
        return compute_stream_checksum(f, algorithm)

validate_checksum_fn

validate_checksum_fn(
    checksum_fn: str, include_extras: bool = True
) -> str

Validate a checksum/hash algorithm name against hashlib + extra algorithms.

Parameters:

Name Type Description Default
checksum_fn str

name of the algorithm to validate, e.g. "md5" or "sha256"

required
include_extras bool

whether to include the non-hashlib algorithms, defaults to True

True

Returns:

Type Description
str

the validated algorithm name, lowercased

Raises:

Type Description
ValueError

if the algorithm is not supported

Source code in src/cdm_data_loaders/utils/checksums.py
def validate_checksum_fn(checksum_fn: str, include_extras: bool = True) -> str:  # noqa: FBT001, FBT002
    """Validate a checksum/hash algorithm name against hashlib + extra algorithms.

    :param checksum_fn: name of the algorithm to validate, e.g. "md5" or "sha256"
    :type checksum_fn: str
    :param include_extras: whether to include the non-hashlib algorithms, defaults to True
    :type include_extras: bool, optional
    :raises ValueError: if the algorithm is not supported
    :return: the validated algorithm name, lowercased
    :rtype: str
    """
    checksum_fn = checksum_fn.lower()
    valid_algorithms = {algo for algo in hashlib.algorithms_available if not algo.startswith("shake")}
    if include_extras:
        valid_algorithms |= EXTRA_ALGORITHMS

    if checksum_fn not in valid_algorithms:
        msg = f"Hashing algorithm {checksum_fn} not supported."
        logger.error(msg)
        raise ValueError(msg)
    return checksum_fn

new_hasher

new_hasher(
    algorithm: str, include_extras: bool = True
) -> Hasher

Construct a Hasher for any supported algorithm (hashlib or crc64nvme).

Parameters:

Name Type Description Default
algorithm str

algorithm name

required
include_extras bool

whether to include the non-hashlib algorithms, defaults to True

True

Returns:

Type Description
Hasher

description

Source code in src/cdm_data_loaders/utils/checksums.py
def new_hasher(algorithm: str, include_extras: bool = True) -> Hasher:  # noqa: FBT001, FBT002
    """Construct a Hasher for any supported algorithm (hashlib or crc64nvme).

    :param algorithm: algorithm name
    :type algorithm: str
    :param include_extras: whether to include the non-hashlib algorithms, defaults to True
    :type include_extras: bool, optional
    :return: _description_
    :rtype: Hasher
    """
    algorithm = validate_checksum_fn(algorithm, include_extras=include_extras)
    if algorithm == "crc64nvme":
        return Crc64NvmeHasher()
    return hashlib.new(algorithm)