"""Raw zone: the immutable download cache, one directory per ``(source, vintage)`` with a sidecar.
Rows and schema are serialized as returned by the connector and never edited in place — a re-pull
with different content is a new ``vintage`` directory, not an overwrite. Parquet and ``_meta.json``
are staged together before the complete directory is atomically published. The sidecar records
what the pull is, its digest contract, and whether it is itself point-in-time. The cache root is
user-configurable and defaults outside the repo (``paths.data_home``); **licensed data lives only
here, never in git**.
"""
from __future__ import annotations
import json
import stat
from dataclasses import asdict, dataclass
from pathlib import Path
from numeraire_dataset.paths import data_home
from numeraire_dataset.zones.steps import CONTENT_DIGEST_VERSION
# A pull is either a dated point-in-time vintage, or a latest-snapshot convenience pull (revised).
PIT_STATUSES = ("vintage", "snapshot")
def _cache_component(value: str, *, label: str) -> str:
"""Reject path traversal before a cache component reaches any writer or cleanup path."""
if (
not value
or value in {".", ".."}
or any(char in value for char in ("/", "\\", "\0", "@", "#"))
):
raise ValueError(f"{label} must be a non-empty path component without reserved separators")
return value
def _raw_path(source: str, vintage: str, *, home: str | Path | None = None) -> Path:
"""Resolve a cache path without creating a vintage directory."""
source = _cache_component(source, label="source")
vintage = _cache_component(vintage, label="vintage")
root = data_home(home).resolve()
candidate = root / "raw" / source / vintage
if not candidate.resolve(strict=False).is_relative_to(root):
raise RuntimeError("raw cache path resolves outside the configured data home")
return candidate
def _validate_cache_file(path: Path, *, label: str) -> Path:
"""Require a final cache artifact to be a regular file, never a symlink."""
if path.is_symlink():
raise RuntimeError(f"{label} is a symlink; inspect the immutable cache manually")
mode = path.lstat().st_mode
if not stat.S_ISREG(mode):
raise RuntimeError(f"{label} is not a regular file; inspect the immutable cache manually")
return path
[docs]
def raw_dir(source: str, vintage: str, *, home: str | Path | None = None) -> Path:
"""Return and create the cache directory for a ``(source, vintage)`` pull."""
directory = _raw_path(source, vintage, home=home)
directory.mkdir(parents=True, exist_ok=True)
return directory
def _write_meta_at(meta: RawMeta, directory: Path) -> Path:
"""Write ``meta`` into an unpublished staging directory."""
directory.mkdir(parents=True, exist_ok=True)
path = directory / "_meta.json"
path.write_text(json.dumps(asdict(meta), sort_keys=True, indent=2), encoding="utf-8")
return path