Source code for app.storage.mapping_input
"""MappingInput checkpoint storage helpers.
`pycolmap.MappingInput.save/load` writes a binary container with magic
`PCMAPIN\\x00` (v1) holding the pre-mapping state needed to resume an
incremental run. We persist these per-job under
`jobs/{job_id}/checkpoints/{seq}.pcmapin` so the worker can reload from
the latest checkpoint on resume without re-running upstream stages.
The pycolmap binding is required to read/write — for tests we expose a
trivial adapter that round-trips a payload through a regular file so the
storage code path can be exercised independently.
"""
from __future__ import annotations
import contextlib
import json
import os
from dataclasses import dataclass
from pathlib import Path
from app.core.errors import StorageError
[docs]
@dataclass(frozen=True)
class CheckpointRef:
seq: int
path: Path
summary: dict
[docs]
def checkpoint_root(job_dir: Path) -> Path:
return job_dir / "checkpoints"
[docs]
def write_checkpoint(
job_dir: Path, *, seq: int, payload: bytes, summary: dict | None = None
) -> CheckpointRef:
root = checkpoint_root(job_dir)
root.mkdir(parents=True, exist_ok=True)
target = root / f"{seq:08d}.pcmapin"
if target.exists():
raise StorageError(f"checkpoint seq {seq} already exists")
tmp = target.with_suffix(".pcmapin.tmp")
tmp.write_bytes(payload)
os.replace(tmp, target)
meta = root / f"{seq:08d}.json"
meta.write_text(json.dumps(summary or {}, sort_keys=True, indent=2), encoding="utf-8")
return CheckpointRef(seq=seq, path=target, summary=summary or {})
[docs]
def list_checkpoints(job_dir: Path) -> list[CheckpointRef]:
root = checkpoint_root(job_dir)
if not root.exists():
return []
out: list[CheckpointRef] = []
for p in sorted(root.glob("*.pcmapin")):
try:
seq = int(p.stem)
except ValueError:
continue
meta_path = root / f"{seq:08d}.json"
summary: dict = {}
if meta_path.is_file():
with contextlib.suppress(json.JSONDecodeError, OSError):
summary = json.loads(meta_path.read_text(encoding="utf-8"))
out.append(CheckpointRef(seq=seq, path=p, summary=summary))
return out
[docs]
def latest_checkpoint(job_dir: Path) -> CheckpointRef | None:
cps = list_checkpoints(job_dir)
return cps[-1] if cps else None
[docs]
def gc_checkpoints(job_dir: Path, *, keep_last: int = 5) -> list[int]:
cps = list_checkpoints(job_dir)
if len(cps) <= keep_last:
return []
to_drop = cps[:-keep_last]
dropped: list[int] = []
for cp in to_drop:
with contextlib.suppress(OSError):
cp.path.unlink()
(cp.path.parent / f"{cp.seq:08d}.json").unlink()
dropped.append(cp.seq)
return dropped