| """Checkpoint manager for recovery.""" |
|
|
| import pickle |
| from datetime import datetime |
| from pathlib import Path |
|
|
| from .models import CheckpointData |
|
|
|
|
| class CheckpointManager: |
| """Saves and restores pipeline state for resume.""" |
|
|
| def __init__(self, checkpoint_dir: Path, enabled: bool = True): |
| self.checkpoint_dir = Path(checkpoint_dir) |
| self.enabled = enabled |
| if self.enabled: |
| self.checkpoint_dir.mkdir(parents=True, exist_ok=True) |
|
|
| def save( |
| self, |
| phase: str, |
| papers: list[dict], |
| papers_with_abstracts: int, |
| custom_data: dict | None = None, |
| ) -> Path: |
| if not self.enabled: |
| return Path() |
|
|
| filepath = self.checkpoint_dir / self._filename(phase) |
| checkpoint = CheckpointData( |
| phase=phase, |
| papers_processed=len(papers), |
| papers_with_abstracts=papers_with_abstracts, |
| custom_data=custom_data or {}, |
| ) |
| with open(filepath, "wb") as fh: |
| pickle.dump({"checkpoint": checkpoint.model_dump(), "papers": papers}, fh) |
|
|
| self._prune_old(phase) |
| return filepath |
|
|
| def load(self, specific_file: Path | None = None) -> tuple[CheckpointData, list[dict]] | None: |
| if not self.enabled: |
| return None |
|
|
| filepath = specific_file or self._latest() |
| if filepath is None or not filepath.exists(): |
| return None |
|
|
| with open(filepath, "rb") as fh: |
| data = pickle.load(fh) |
|
|
| return CheckpointData(**data["checkpoint"]), data.get("papers", []) |
|
|
| def list_checkpoints(self) -> list[Path]: |
| if not self.checkpoint_dir.exists(): |
| return [] |
| return sorted(self.checkpoint_dir.glob("checkpoint_*.pkl"), reverse=True) |
|
|
| def clear(self) -> int: |
| count = 0 |
| for cp in self.list_checkpoints(): |
| cp.unlink() |
| count += 1 |
| return count |
|
|
| def _filename(self, phase: str) -> str: |
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") |
| return f"checkpoint_{phase}_{ts}.pkl" |
|
|
| def _latest(self) -> Path | None: |
| checkpoints = self.list_checkpoints() |
| return checkpoints[0] if checkpoints else None |
|
|
| def _prune_old(self, phase: str, keep: int = 5) -> None: |
| pattern = f"checkpoint_{phase}_*.pkl" |
| for old in sorted(self.checkpoint_dir.glob(pattern), reverse=True)[keep:]: |
| old.unlink() |
|
|