"""Migration utility: convert Python literal config values in the DB into canonical JSON. Usage: python scripts/migrate_config_literals.py [--apply] [--backup=path] [--quiet] By default the script runs in dry-run mode and prints candidate rows it would change. Use --apply to persist changes. --backup writes a JSON file listing changed rows before applying. """ from __future__ import annotations import argparse import sqlite3 import json import ast from pathlib import Path from typing import Any, Dict, List, Tuple DB = Path("medios.db") def _is_json_like(s: str) -> bool: if not isinstance(s, str): return False s = s.strip() if not s: return False return s[0] in '{["' or s.lower() in ("true", "false", "null") or s[0].isdigit() or s[0] == "'" def find_candidates(conn: sqlite3.Connection) -> List[Tuple[int, str, str, str, str, str]]: cur = conn.cursor() cur.execute("SELECT rowid, category, subtype, item_name, key, value FROM config") rows = [] for rowid, cat, sub, name, key, val in cur.fetchall(): if val is None: continue s = str(val) if _is_json_like(s): try: json.loads(s) except Exception: # Try ast.literal_eval try: parsed = ast.literal_eval(s) # Only consider basic JSON-serializable types json.dumps(parsed) rows.append((rowid, cat, sub, name, key, s)) except Exception: continue return rows def apply_migration(conn: sqlite3.Connection, candidates: List[Tuple[int, str, str, str, str, str]]) -> List[Tuple[int, str, str, str, str, str]]: cur = conn.cursor() changed = [] for row in candidates: rowid, cat, sub, name, key, val = row try: parsed = ast.literal_eval(val) new_val = json.dumps(parsed, ensure_ascii=False) cur.execute("UPDATE config SET value = ? WHERE rowid = ?", (new_val, rowid)) changed.append((rowid, cat, sub, name, key, new_val)) except Exception: continue conn.commit() return changed def main(): parser = argparse.ArgumentParser() parser.add_argument("--apply", action="store_true", help="Persist changes to DB") parser.add_argument("--backup", type=str, default=None, help="Path to write backup JSON of changed rows") parser.add_argument("--quiet", action="store_true", help="Minimize output") args = parser.parse_args() conn = sqlite3.connect(str(DB)) candidates = find_candidates(conn) if not args.quiet: print(f"Found {len(candidates)} candidate rows for migration") for r in candidates[:50]: rowid, cat, sub, name, key, val = r print(f"row {rowid}: {cat}.{sub}.{name} {key} -> {val[:200]!r}") if not candidates: return 0 if args.backup: out_path = Path(args.backup) data = [dict(rowid=r[0], category=r[1], subtype=r[2], item_name=r[3], key=r[4], value=r[5]) for r in candidates] out_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") if not args.quiet: print(f"Wrote backup to {out_path}") if args.apply: changed = apply_migration(conn, candidates) if not args.quiet: print(f"Applied migration to {len(changed)} rows") return 0 if not args.quiet: print("Dry-run; re-run with --apply to persist changes") return 0 if __name__ == "__main__": raise SystemExit(main())