GNN4Colliders / validation /fixture.py
ho22joshua's picture
PR 2 — Portable Public End-to-End Validation Fixture (#11)
c20af2a
Raw History Blame Contribute Delete
3.37 kB
"""Pinned public validation-fixture discovery, download, and verification."""
from __future__ import annotations
import hashlib
import os
import tempfile
import urllib.request
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class PublicFixture:
repository: str
revision: str
repository_path: str
sha256: str
size_bytes: int
@property
def url(self) -> str:
return (
f"https://huggingface.co/datasets/{self.repository}/resolve/"
f"{self.revision}/{self.repository_path}?download=true"
)
TESTING_FIXTURE = PublicFixture(
repository="HWresearch/Delphes",
revision="76a6c362bfba8e766ba7255a5c08a55257f78d0e",
repository_path="testing/ttH_NLO_64.root",
sha256="89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec",
size_bytes=15050,
)
def default_fixture_path(root: str | Path = ".") -> Path:
return Path(root) / "data" / "fixtures" / "testing" / "ttH_NLO_64.root"
def file_sha256(path: str | Path) -> str:
digest = hashlib.sha256()
with Path(path).open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def verify_fixture(path: str | Path, fixture: PublicFixture = TESTING_FIXTURE) -> Path:
"""Verify size and SHA-256 before allowing a fixture into validation."""
path = Path(path)
if not path.is_file():
raise FileNotFoundError(f"validation fixture is missing: {path}")
actual_size = path.stat().st_size
if actual_size != fixture.size_bytes:
raise ValueError(
f"fixture size mismatch for {path}: expected {fixture.size_bytes}, "
f"got {actual_size}"
)
actual_sha256 = file_sha256(path)
if actual_sha256 != fixture.sha256:
raise ValueError(
f"fixture SHA-256 mismatch for {path}: expected {fixture.sha256}, "
f"got {actual_sha256}"
)
return path
def download_fixture(
path: str | Path,
fixture: PublicFixture = TESTING_FIXTURE,
*,
timeout: float = 60.0,
) -> Path:
"""Download a pinned fixture atomically and verify it before returning."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary_name = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".download", dir=path.parent
)
os.close(fd)
temporary = Path(temporary_name)
try:
with urllib.request.urlopen(fixture.url, timeout=timeout) as response:
with temporary.open("wb") as output:
while chunk := response.read(1024 * 1024):
output.write(chunk)
verify_fixture(temporary, fixture)
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
return path
def ensure_fixture(
path: str | Path | None = None,
*,
download: bool = True,
fixture: PublicFixture = TESTING_FIXTURE,
) -> Path:
"""Return a verified local fixture, downloading it when necessary."""
path = Path(path) if path is not None else default_fixture_path()
if path.is_file():
return verify_fixture(path, fixture)
if not download:
raise FileNotFoundError(f"validation fixture is missing: {path}")
return download_fixture(path, fixture)