You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
"""Snapshot registry, alias resolution, and canonical path helpers."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from libs.common.config import get_settings
|
|
|
|
_REGISTRY_PATH = Path("configs/snapshots/registry.json")
|
|
_LEGACY_SNAPSHOT_FALLBACK_ROOT = Path("data/datasets/snapshots")
|
|
|
|
|
|
class UnknownSnapshotError(ValueError):
|
|
"""Raised when a snapshot id cannot be resolved."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SnapshotResolution:
|
|
requested_snapshot_id: str
|
|
canonical_snapshot_id: str
|
|
refresh_policy: str | None
|
|
purpose: str | None
|
|
expected_feature_set: str | None
|
|
is_alias: bool
|
|
is_registry_managed: bool
|
|
is_legacy_passthrough: bool
|
|
|
|
|
|
def _load_registry_payload() -> dict[str, Any]:
|
|
if not _REGISTRY_PATH.exists():
|
|
raise FileNotFoundError(f"Snapshot registry not found: {_REGISTRY_PATH}")
|
|
return json.loads(_REGISTRY_PATH.read_text())
|
|
|
|
|
|
def load_snapshot_registry() -> dict[str, Any]:
|
|
"""Return the raw snapshot registry payload."""
|
|
return _load_registry_payload()
|
|
|
|
|
|
def get_canonical_spec(canonical_snapshot_id: str) -> dict[str, Any]:
|
|
"""Return the registry spec for a managed canonical snapshot."""
|
|
payload = _load_registry_payload()
|
|
spec = payload.get("canonicals", {}).get(canonical_snapshot_id)
|
|
if spec is None:
|
|
raise UnknownSnapshotError(f"Unknown canonical snapshot id: {canonical_snapshot_id}")
|
|
return dict(spec)
|
|
|
|
|
|
def get_requested_aliases(canonical_snapshot_id: str) -> list[str]:
|
|
"""Return all legacy aliases that resolve to *canonical_snapshot_id*."""
|
|
payload = _load_registry_payload()
|
|
aliases = payload.get("aliases", {})
|
|
return sorted(
|
|
alias
|
|
for alias, alias_spec in aliases.items()
|
|
if alias_spec.get("canonical_id") == canonical_snapshot_id
|
|
)
|
|
|
|
|
|
def _snapshot_root_candidates(snapshot_dir: str | Path | None = None) -> list[Path]:
|
|
if snapshot_dir is not None:
|
|
return [Path(snapshot_dir)]
|
|
settings = get_settings()
|
|
roots = [Path(settings.parquet_dir), _LEGACY_SNAPSHOT_FALLBACK_ROOT]
|
|
deduped: list[Path] = []
|
|
seen: set[Path] = set()
|
|
for root in roots:
|
|
root = root.resolve()
|
|
if root in seen:
|
|
continue
|
|
seen.add(root)
|
|
deduped.append(root)
|
|
return deduped
|
|
|
|
|
|
def snapshot_exists(snapshot_id: str, snapshot_dir: str | Path | None = None) -> bool:
|
|
"""Return True when the raw snapshot directory exists in any known root."""
|
|
for root in _snapshot_root_candidates(snapshot_dir):
|
|
if (root / snapshot_id).exists():
|
|
return True
|
|
return False
|
|
|
|
|
|
def resolve_snapshot(
|
|
requested_snapshot_id: str,
|
|
*,
|
|
snapshot_dir: str | Path | None = None,
|
|
allow_legacy_passthrough: bool = True,
|
|
) -> SnapshotResolution:
|
|
"""Resolve a requested snapshot id to its canonical snapshot id.
|
|
|
|
Registry-managed aliases map to one of the two canonicals. For legacy
|
|
directories outside the managed alias set, phase-1 keeps a passthrough
|
|
fallback so old experiments do not break immediately.
|
|
"""
|
|
payload = _load_registry_payload()
|
|
canonicals = payload.get("canonicals", {})
|
|
aliases = payload.get("aliases", {})
|
|
|
|
if requested_snapshot_id in canonicals:
|
|
spec = canonicals[requested_snapshot_id]
|
|
return SnapshotResolution(
|
|
requested_snapshot_id=requested_snapshot_id,
|
|
canonical_snapshot_id=requested_snapshot_id,
|
|
refresh_policy=spec.get("refresh_policy"),
|
|
purpose=spec.get("purpose"),
|
|
expected_feature_set=None,
|
|
is_alias=False,
|
|
is_registry_managed=True,
|
|
is_legacy_passthrough=False,
|
|
)
|
|
|
|
if requested_snapshot_id in aliases:
|
|
alias_spec = aliases[requested_snapshot_id]
|
|
canonical_snapshot_id = str(alias_spec["canonical_id"])
|
|
canonical_spec = canonicals.get(canonical_snapshot_id, {})
|
|
return SnapshotResolution(
|
|
requested_snapshot_id=requested_snapshot_id,
|
|
canonical_snapshot_id=canonical_snapshot_id,
|
|
refresh_policy=canonical_spec.get("refresh_policy"),
|
|
purpose=alias_spec.get("purpose") or canonical_spec.get("purpose"),
|
|
expected_feature_set=alias_spec.get("expected_feature_set"),
|
|
is_alias=True,
|
|
is_registry_managed=True,
|
|
is_legacy_passthrough=False,
|
|
)
|
|
|
|
if allow_legacy_passthrough and snapshot_exists(requested_snapshot_id, snapshot_dir=snapshot_dir):
|
|
return SnapshotResolution(
|
|
requested_snapshot_id=requested_snapshot_id,
|
|
canonical_snapshot_id=requested_snapshot_id,
|
|
refresh_policy="legacy_passthrough",
|
|
purpose="legacy_passthrough",
|
|
expected_feature_set=None,
|
|
is_alias=False,
|
|
is_registry_managed=False,
|
|
is_legacy_passthrough=True,
|
|
)
|
|
|
|
raise UnknownSnapshotError(f"Unknown snapshot id: {requested_snapshot_id}")
|
|
|
|
|
|
def resolve_snapshot_path(
|
|
requested_snapshot_id: str,
|
|
*,
|
|
snapshot_dir: str | Path | None = None,
|
|
allow_legacy_passthrough: bool = True,
|
|
) -> Path | None:
|
|
"""Resolve the on-disk snapshot directory.
|
|
|
|
Prefer the canonical directory. If it does not exist yet, fall back to the
|
|
requested legacy alias directory so phase-1 remains parity-safe until the
|
|
canonical snapshot has been rebuilt.
|
|
"""
|
|
resolution = resolve_snapshot(
|
|
requested_snapshot_id,
|
|
snapshot_dir=snapshot_dir,
|
|
allow_legacy_passthrough=allow_legacy_passthrough,
|
|
)
|
|
candidate_ids = [resolution.canonical_snapshot_id]
|
|
if resolution.requested_snapshot_id != resolution.canonical_snapshot_id:
|
|
candidate_ids.append(resolution.requested_snapshot_id)
|
|
|
|
seen: set[Path] = set()
|
|
for root in _snapshot_root_candidates(snapshot_dir):
|
|
for snapshot_id in candidate_ids:
|
|
candidate = (root / snapshot_id).resolve()
|
|
if candidate in seen:
|
|
continue
|
|
seen.add(candidate)
|
|
if candidate.exists():
|
|
return candidate
|
|
return None
|
|
|
|
|
|
def canonical_snapshot_dir(canonical_snapshot_id: str) -> Path:
|
|
"""Return the managed canonical directory under data/parquet."""
|
|
settings = get_settings()
|
|
return Path(settings.parquet_dir).resolve() / canonical_snapshot_id
|
|
|