← back to Designerwallcoverings

scripts/c34-osborne-gaston/enqueue-specs.py

98 lines

#!/usr/bin/env python3
"""
c34 Osborne & Little + Gaston Y Daniela specs metafield push.
Live-blank-verify at enqueue time (officer-required guard), build restore-map,
emit NOT-EXISTS-guarded shopify_api_queue INSERT SQL. Read-only by itself —
prints SQL + restore-map; the SQL is run on Kamatera as the gated step.

Usage:
  enqueue-specs.py --worklist <tsv> --limit N   # canary: first N products
  enqueue-specs.py --worklist <tsv>             # all
Writes: restore-map-<ts>.csv, enqueue-<ts>.sql  (does NOT touch any DB)
"""
import sys, os, csv, json, urllib.request, argparse, datetime

STORE = "designer-laboratory-sandbox.myshopify.com"  # = real prod www.designerwallcoverings.com
API = "2024-10"

def token():
    for line in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
        if line.startswith("SHOPIFY_ADMIN_TOKEN="):
            return line.strip().split("=", 1)[1]
    sys.exit("no SHOPIFY_ADMIN_TOKEN")

def live_metafields(pid, tok):
    url = f"https://{STORE}/admin/api/{API}/products/{pid}/metafields.json"
    req = urllib.request.Request(url, headers={"X-Shopify-Access-Token": tok})
    ms = json.load(urllib.request.urlopen(req, timeout=30)).get("metafields", [])
    cur = {}
    for m in ms:
        if m.get("namespace") == "custom" and m.get("key") in ("width", "material"):
            cur[m["key"]] = m.get("value")
    return cur

def sql_lit(s):
    return "'" + s.replace("'", "''") + "'"

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--worklist", required=True)
    ap.add_argument("--limit", type=int, default=0)
    a = ap.parse_args()
    tok = token()
    ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    rows = list(csv.DictReader(open(a.worklist), delimiter="\t"))
    # one product per row in this worklist; group unique product_ids preserving order
    seen, products = set(), []
    for r in rows:
        if r["product_id"] not in seen:
            seen.add(r["product_id"]); products.append(r)
    if a.limit:
        products = products[:a.limit]

    restore, inserts, skipped = [], [], []
    for r in products:
        pid = r["product_id"]
        cur = live_metafields(pid, tok)
        restore.append({"product_id": pid, "handle": r["handle"], "vendor": r["vendor"],
                        "live_width": cur.get("width", ""), "live_material": cur.get("material", "")})
        for key, proposed in (("width", r["proposed_width"]), ("material", r["proposed_material"])):
            if not proposed or proposed.startswith(("EXCLUDE", "SKIP")) or proposed in ("(blank)", "N/A"):
                continue
            live_val = cur.get(key)
            if live_val:  # live-blank guard: never overwrite an existing live value
                skipped.append(f"{pid} {key}: live already = {live_val!r} (proposed {proposed!r}) -> SKIP")
                continue
            payload = json.dumps({"metafield": {"namespace": "custom", "key": key,
                                  "type": "single_line_text_field", "value": proposed}})
            endpoint = f"/products/{pid}/metafields.json"
            inserts.append((endpoint, payload, key, pid, proposed))

    rm = f"restore-map-{ts}.csv"
    with open(rm, "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=["product_id", "handle", "vendor", "live_width", "live_material"])
        w.writeheader(); w.writerows(restore)

    sqlf = f"enqueue-{ts}.sql"
    with open(sqlf, "w") as f:
        f.write("-- c34 osborne/gaston specs metafield enqueue (live-blank-verified). Run on Kamatera.\nBEGIN;\n")
        for endpoint, payload, key, pid, proposed in inserts:
            f.write(
                "INSERT INTO shopify_api_queue (method, endpoint, payload, token_alias, priority, status, source_agent)\n"
                f"SELECT 'POST', {sql_lit(endpoint)}, {sql_lit(payload)}::jsonb, 'product', 7, 'pending', 'c34-osborne-gaston-specs'\n"
                "WHERE NOT EXISTS (SELECT 1 FROM shopify_api_queue q WHERE q.request_hash = "
                f"md5(('POST'||{sql_lit(endpoint)})||COALESCE(({sql_lit(payload)}::jsonb)::text,'')) "
                "AND q.status IN ('pending','processing'));\n")
        f.write("COMMIT;\n")

    print(f"products checked: {len(products)}")
    print(f"genuinely-blank writes to enqueue: {len(inserts)}  (width={sum(1 for i in inserts if i[2]=='width')}, material={sum(1 for i in inserts if i[2]=='material')})")
    print(f"skipped (live already filled): {len(skipped)}")
    for s in skipped[:10]:
        print("  ", s)
    print(f"restore-map: {rm}")
    print(f"enqueue SQL: {sqlf}")

if __name__ == "__main__":
    main()