#!/usr/bin/env python3 """ Import scripts/generated-model-content.json into the model_content table. Reads each row and upserts (brand_slug, model_slug) -> pdp_body, opener_variants, closer_variants, style_tags, ai_model, ai_generated_at. Does NOT touch intro / description / faq columns (owned by the template-based subtype page generator). Run migration 010_model_content_ai.sql first. Usage: python scripts/import-model-content.py # import all rows python scripts/import-model-content.py --dry-run # show what would change python scripts/import-model-content.py --brand nike # filter per merk """ import os import sys import json import argparse from pathlib import Path import psycopg2 import psycopg2.extras from dotenv import load_dotenv load_dotenv() DB_URL = os.environ["DATABASE_URL"] HERE = Path(__file__).parent INPUT = HERE / "generated-model-content.json" AI_MODEL_TAG = "claude-opus-4-7-manual" def load_rows(): with open(INPUT, "r", encoding="utf-8") as f: return json.load(f) def upsert(cur, row): cur.execute( """ INSERT INTO model_content ( brand_slug, model_slug, model_name, pdp_body, opener_variants, closer_variants, style_tags, ai_model, ai_generated_at ) VALUES (%s, %s, %s, %s, %s::jsonb, %s::jsonb, %s::jsonb, %s, NOW()) ON CONFLICT (brand_slug, model_slug) DO UPDATE SET model_name = EXCLUDED.model_name, pdp_body = EXCLUDED.pdp_body, opener_variants = EXCLUDED.opener_variants, closer_variants = EXCLUDED.closer_variants, style_tags = EXCLUDED.style_tags, ai_model = EXCLUDED.ai_model, ai_generated_at = EXCLUDED.ai_generated_at, updated_at = NOW() """, ( row["brand_slug"], row["model_slug"], row["model_name"], row["pdp_body"], json.dumps(row.get("opener_variants") or []), json.dumps(row.get("closer_variants") or []), json.dumps(row.get("style_tags") or []), AI_MODEL_TAG, ), ) def main(): parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true") parser.add_argument("--brand", type=str, default=None) args = parser.parse_args() rows = load_rows() if args.brand: rows = [r for r in rows if r["brand_slug"] == args.brand] print(f"Importing {len(rows)} rows from {INPUT}") if args.dry_run: print("DRY RUN — no DB writes") conn = psycopg2.connect(DB_URL) conn.autocommit = False cur = conn.cursor() # Sanity check: the model_content table must have the new columns. cur.execute( """ SELECT column_name FROM information_schema.columns WHERE table_name = 'model_content' AND column_name IN ('pdp_body','opener_variants','closer_variants','style_tags','ai_model','ai_generated_at') """ ) present = {r[0] for r in cur.fetchall()} expected = {"pdp_body", "opener_variants", "closer_variants", "style_tags", "ai_model", "ai_generated_at"} missing = expected - present if missing: print(f"ERROR: model_content is missing columns: {sorted(missing)}", file=sys.stderr) print("Run: psql -d $DATABASE -f db/migrations/010_model_content_ai.sql", file=sys.stderr) sys.exit(2) ok = 0 fail = 0 for idx, row in enumerate(rows, 1): label = f"{row['brand_slug']}/{row['model_slug']}" try: if not args.dry_run: upsert(cur, row) ok += 1 print(f"[{idx}/{len(rows)}] OK {label}") except Exception as e: fail += 1 print(f"[{idx}/{len(rows)}] FAIL {label}: {e}", file=sys.stderr) conn.rollback() if args.dry_run: conn.rollback() else: conn.commit() conn.close() print(f"\nDone. ok={ok} fail={fail}") if __name__ == "__main__": main()