Source code for flpq_data.dataset.data

"""Download graph data from dataset."""

import hashlib
import logging
import os
import pathlib
import shutil
import tempfile
import warnings

import requests

from flpq_data.config import DATASET_VERSION
from flpq_data.dataset.cache import version_dir
from flpq_data.dataset.registry import GraphInfo, graph_info

__all__ = [
    "DATASET_KEY_PREFIX",
    "DATASET_URL",
    "graph_dir",
    "download_graph",
    "download",
]

DATASET_KEY_PREFIX = f"{DATASET_VERSION}/graph"
DATASET_URL = f"https://cfpq-data.storage.yandexcloud.net/{DATASET_KEY_PREFIX}/"


def _sha256(path: pathlib.Path) -> str:
    """Returns the streaming sha256 hex digest of a file."""
    hasher = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


def _install(name: str, info: GraphInfo, dest: pathlib.Path) -> None:
    """Download, verify, and install the archive of ``name`` into ``dest``.

    The download is streamed to a temp file with a running sha256; only a
    verified archive is unpacked and moved into place, so any failure leaves
    ``dest`` untouched and removes the temp files.
    """
    parent = dest.parent
    parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_archive_str = tempfile.mkstemp(dir=parent, suffix=".tar.gz")
    os.close(fd)
    tmp_archive = pathlib.Path(tmp_archive_str)
    tmp_extract: pathlib.Path | None = None
    try:
        logging.info(f"Downloading {DATASET_URL}{name}.tar.gz")
        with requests.get(url=DATASET_URL + f"{name}.tar.gz", stream=True) as r:
            r.raise_for_status()
            hasher = hashlib.sha256()
            with tmp_archive.open("wb") as out:
                for chunk in iter(lambda: r.raw.read(1 << 20), b""):
                    hasher.update(chunk)
                    out.write(chunk)
        digest = hasher.hexdigest()
        if digest != info.sha256:
            raise ValueError(
                f"sha256 mismatch for {name}: expected {info.sha256}, got {digest}"
            )

        tmp_extract = pathlib.Path(tempfile.mkdtemp(dir=parent))
        shutil.unpack_archive(tmp_archive, tmp_extract)

        entries = list(tmp_extract.iterdir())
        if len(entries) != 1 or not entries[0].is_dir():
            raise ValueError(
                f"Archive for {name=} must contain a single "
                f"top-level directory, found {entries=}"
            )

        if dest.exists():
            shutil.rmtree(dest)
        shutil.move(str(entries[0]), str(dest))
        shutil.move(str(tmp_archive), str(dest / f"{name}.tar.gz"))
        (dest / f"{name}.tar.gz.sha256").write_text(digest + "\n")

        logging.info(f"Installed graph {name=} to directory {dest=}")
    finally:
        if tmp_archive.exists():
            tmp_archive.unlink()
        if tmp_extract is not None and tmp_extract.exists():
            shutil.rmtree(tmp_extract, ignore_errors=True)


[docs] def graph_dir(name: str, verify: bool = False) -> pathlib.Path: """Return the local directory of a graph, downloading it on a cache miss. The data lives in the machine-global cache (see :func:`cache_root`), mirroring the dataset layout:: <root>/<version>/graph/<name>/<name>.tar.gz plus the unpacked contents in the same folder A cache hit — the folder and archive are present and the digest matches the registry — returns the path with zero network. The digest is read from the ``<name>.tar.gz.sha256`` sidecar written at install; ``verify=True`` re-hashes the archive instead of trusting the sidecar. On a miss the archive is downloaded, its sha256 is verified against the registry while streaming, it is unpacked (a single top-level directory is required), and the result is moved into the cache; any failure leaves the cache untouched. Parameters ---------- name : str The name of the graph from the dataset. verify : bool, optional Re-hash the cached archive against the registry instead of trusting the sidecar digest (default ``False``). Examples -------- >>> from flpq_data import * >>> path = graph_dir("generations") >>> path.name 'generations' Returns ------- path : Path Path to the directory with the graph data. Raises ------ FileNotFoundError If no graph with this name exists in the dataset. """ info = graph_info(name) dest = version_dir() / "graph" / name archive = dest / f"{name}.tar.gz" sidecar = archive.with_name(archive.name + ".sha256") if dest.is_dir() and archive.is_file(): if verify: digest = _sha256(archive) else: digest = sidecar.read_text().strip() if sidecar.is_file() else None if digest == info.sha256: logging.info(f"Cache hit for graph {name=}") return dest _install(name, info, dest) return dest
[docs] def download_graph(name: str) -> pathlib.Path: """Deprecated alias of :func:`graph_dir`. .. deprecated:: 6.0.0 Use :func:`graph_dir` instead. """ warnings.warn( "download_graph() is deprecated, use graph_dir() instead", DeprecationWarning, stacklevel=2, ) return graph_dir(name)
def download(name: str) -> pathlib.Path: """Deprecated alias of :func:`graph_dir`. .. deprecated:: 6.0.0 Use :func:`graph_dir` instead. """ warnings.warn( "download() is deprecated, use graph_dir() instead", DeprecationWarning, stacklevel=2, ) return graph_dir(name)