#!/usr/bin/env python3 """ Backfill sx_content from lexical JSON for all posts that have lexical but no sx_content. Usage: python -m blog.scripts.backfill_sx_content [--dry-run] """ from __future__ import annotations import argparse import asyncio import sys from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession async def backfill(dry_run: bool = False) -> int: from shared.db.session import get_session from models.ghost_content import Post from bp.blog.ghost.lexical_to_sx import lexical_to_sx converted = 0 errors = 0 async with get_session() as sess: stmt = select(Post).where( and_( Post.lexical.isnot(None), Post.lexical != "", (Post.sx_content.is_(None)) | (Post.sx_content == ""), ) ) result = await sess.execute(stmt) posts = result.scalars().all() print(f"Found {len(posts)} posts to convert") for post in posts: try: sx = lexical_to_sx(post.lexical) if dry_run: print(f" [DRY RUN] {post.slug}: {len(sx)} chars") else: post.sx_content = sx print(f" Converted: {post.slug} ({len(sx)} chars)") converted += 1 except Exception as e: print(f" ERROR: {post.slug}: {e}", file=sys.stderr) errors += 1 if not dry_run: await sess.commit() print(f"\nDone: {converted} converted, {errors} errors") return converted def main(): parser = argparse.ArgumentParser(description="Backfill sx_content from lexical JSON") parser.add_argument("--dry-run", action="store_true", help="Don't write to database") args = parser.parse_args() asyncio.run(backfill(dry_run=args.dry_run)) if __name__ == "__main__": main()