Files
rose-ash/blog/scripts/backfill_sx_content.py
2026-03-01 23:29:26 +00:00

69 lines
1.9 KiB
Python

#!/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()