eurlex-chat-api / scripts /checkpoint_restore.py
nedaktov-ops
phase-1: CI workflow, ruff linting, keepalive alerting, VITE_API_URL, pre-commit hooks
f3e893e
Raw
History Blame Contribute Delete
6.59 kB
#!/usr/bin/env python3
"""Restore project files from a saved checkpoint.
Supports restoring individual checkpoints, rolling back to the latest checkpoint
for a specific phase, or listing available checkpoints.
Usage:
python3 scripts/checkpoint_restore.py --id ckpt-20260523-220000 # Restore specific checkpoint
python3 scripts/checkpoint_restore.py --latest # Restore latest checkpoint
python3 scripts/checkpoint_restore.py --phase 0 # Restore latest Phase 0 checkpoint
python3 scripts/checkpoint_restore.py --list # List available checkpoints
python3 scripts/checkpoint_restore.py --id ckpt-xxx --dry-run # Preview without restoring
Checkpoints stored in: .checkpoints/<checkpoint_id>/
"""
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
CHECKPOINT_DIR = PROJECT_ROOT / ".checkpoints"
def load_index() -> dict:
"""Load the checkpoint index file."""
index_path = CHECKPOINT_DIR / "index.json"
if not index_path.exists():
print("ERROR: No checkpoints found (index.json missing)", file=sys.stderr)
sys.exit(1)
with open(index_path) as f:
return json.load(f)
def load_checkpoint_meta(checkpoint_id: str) -> dict:
"""Load metadata for a specific checkpoint."""
meta_path = CHECKPOINT_DIR / checkpoint_id / "checkpoint.json"
if not meta_path.exists():
print(f"ERROR: Checkpoint '{checkpoint_id}' not found", file=sys.stderr)
sys.exit(1)
with open(meta_path) as f:
return json.load(f)
def list_checkpoints():
"""List all checkpoints with details."""
index = load_index()
ckpts = index.get("checkpoints", [])
if not ckpts:
print("No checkpoints found")
return
print(f"Available checkpoints ({len(ckpts)}):")
print(f"{'ID':<30} {'Phase':<12} {'Files':<6} {'Message'}")
print("-" * 80)
for c in ckpts:
cid = c["id"]
phase = c.get("phase", "?")
count = c.get("files_count", "?")
msg = c.get("message", "")[:40]
print(f"{cid:<30} {phase:<12} {count:<6} {msg}")
def find_checkpoint(checkpoint_id: str = None, phase: str = None, latest: bool = False) -> str:
"""Find a checkpoint ID based on search criteria."""
index = load_index()
ckpts = index.get("checkpoints", [])
if checkpoint_id:
return checkpoint_id
if latest:
if not ckpts:
print("ERROR: No checkpoints available", file=sys.stderr)
sys.exit(1)
return ckpts[0]["id"]
if phase:
phase_ckpts = [c for c in ckpts if c.get("phase") == phase]
if not phase_ckpts:
print(f"ERROR: No checkpoints found for phase '{phase}'", file=sys.stderr)
sys.exit(1)
return phase_ckpts[0]["id"]
return ckpts[0]["id"]
def restore_checkpoint(checkpoint_id: str, dry_run: bool = False):
"""Restore files from a checkpoint.
Args:
checkpoint_id: The checkpoint ID to restore from.
dry_run: If True, only show what would be restored without copying.
"""
meta = load_checkpoint_meta(checkpoint_id)
ckpt_dir = CHECKPOINT_DIR / checkpoint_id
print(f"Checkpoint: {checkpoint_id}")
print(f" Phase: {meta['phase']}")
print(f" Timestamp: {meta['timestamp']}")
print(f" Message: {meta.get('message', '')}")
print()
if dry_run:
print("DRY RUN — No files will be modified")
print()
files_to_restore = meta.get("files_backed_up", [])
restored = []
skipped = []
for rel_path in files_to_restore:
src = ckpt_dir / rel_path
dst = PROJECT_ROOT / rel_path
if not src.exists():
skipped.append((rel_path, "source missing in checkpoint"))
continue
if dry_run:
dst_exists = dst.exists()
dst_size = os.path.getsize(dst) if dst_exists else 0
src_size = os.path.getsize(src)
print(f" Would restore: {rel_path}")
print(f" From: {src_size} bytes (checkpoint)")
print(f" To: {dst_size} bytes {'(existing)' if dst_exists else '(new file)'}")
restored.append(rel_path)
continue
# Make backup of current file before overwriting?
# No — this is a restore, the checkpoint is the reference.
# If user wants to keep current state, they should have saved a checkpoint first.
os.makedirs(dst.parent, exist_ok=True)
try:
shutil.copy2(src, dst)
restored.append(rel_path)
print(f" ✓ {rel_path}")
except Exception as e:
skipped.append((rel_path, str(e)))
if not dry_run:
print()
print(f"Restore complete: {len(restored)} files restored")
if skipped:
print(f" {len(skipped)} files skipped:")
for f, reason in skipped:
print(f" ✗ {f} ({reason})")
# Update current checkpoint metadata
print()
print(f"Restored from checkpoint: {checkpoint_id}")
print(f" Phase: {meta['phase']}")
print(f" Original timestamp: {meta['timestamp']}")
# If there are app files, suggest restart
has_app = any(f.startswith("app/") for f in restored)
if has_app:
print()
print("⚠️ App files were restored. Restart the API server:")
else:
print()
print(f"Dry run complete: {len(restored)} files would be restored")
if skipped:
print(f" {len(skipped)} files would be skipped")
def main():
parser = argparse.ArgumentParser(
description="Restore EUR-Lex AI Chat project from a saved checkpoint"
)
parser.add_argument("--id", help="Checkpoint ID to restore")
parser.add_argument("--latest", action="store_true", help="Restore latest checkpoint")
parser.add_argument("--phase", help="Restore latest checkpoint for a specific phase")
parser.add_argument("--list", action="store_true", help="List available checkpoints")
parser.add_argument("--dry-run", action="store_true", help="Preview without restoring")
args = parser.parse_args()
if args.list:
list_checkpoints()
return
checkpoint_id = find_checkpoint(
checkpoint_id=args.id,
phase=args.phase,
latest=args.latest,
)
restore_checkpoint(checkpoint_id, dry_run=args.dry_run)
if __name__ == "__main__":
main()