← back to Koroseal Goodrich Audit

execute_mfr_width_backfill.py

78 lines

#!/usr/bin/env python3
"""Koroseal manufacturer_sku(custom+dwc) + global.width additive backfill.
PostgreSQL-first (local dw_unified mirror) THEN Shopify metafieldsSet (authoritative).
Additive, only-where-empty. NO price/status change. Default DRY-RUN.
Usage: execute_mfr_width_backfill.py [--apply]
"""
import json,os,sys,subprocess,time,urllib.request,collections
APPLY="--apply" in sys.argv
ROOT="/Users/macstudio3/Projects/koroseal-goodrich-audit"
WL=os.path.join(ROOT,"mfr_width_writelist.jsonl")
def env(k):
    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
        if l.startswith(k+"="): return l.split("=",1)[1].strip()
TOKEN=env("SHOPIFY_ADMIN_TOKEN"); SHOP=env("SHOPIFY_STORE"); VER="2024-10"
writes=[json.loads(l) for l in open(WL)]
nd=sum(1 for w in writes if "dwc.manufacturer_sku" in w["fields"])
nw=sum(1 for w in writes if "global.width" in w["fields"])
print(f"[{'APPLY' if APPLY else 'DRY-RUN'}] products={len(writes)} dwc.manufacturer_sku={nd} global.width={nw} total_mf={nd+nw}")
print(f"  shop={SHOP} api={VER}  cost=$0 (Shopify Admin API, no metered spend)")

def ns_key(k): ns,key=k.split("."); return ns,key
# ---- PG-first: update local mirror metafields jsonb (only-where-empty) ----
def pg_update(w):
    sets=[]
    for k,v in w["fields"].items():
        ns,key=ns_key(k)
        node=json.dumps({"type":"single_line_text_field","value":v}).replace("'","''")
        # only set if that namespace.key value is currently empty/absent
        sets.append((ns,key,node))
    sid=w["shopify_id"]
    for ns,key,node in sets:
        sql=(f"update shopify_products set metafields = jsonb_set("
             f"coalesce(metafields,'{{}}'::jsonb) || "
             f"jsonb_build_object('{ns}', coalesce(metafields->'{ns}','{{}}'::jsonb)), "
             f"'{{{ns},{key}}}', '{node}'::jsonb, true) "
             f"where shopify_id='{sid}' "
             f"and coalesce(nullif(trim(metafields->'{ns}'->'{key}'->>'value'),''),'')='';")
        if APPLY:
            r=subprocess.run(["psql","host=/tmp dbname=dw_unified","-tA","-c",sql],capture_output=True,text=True)
            if r.returncode!=0: print("  PG ERR",sid,r.stderr.strip()[:120])
    return len(sets)

# ---- Shopify metafieldsSet ----
def shopify_set(batch):
    mf=[]
    for w in batch:
        for k,v in w["fields"].items():
            ns,key=ns_key(k)
            mf.append({"ownerId":w["shopify_id"],"namespace":ns,"key":key,
                       "type":"single_line_text_field","value":v})
    q="""mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){userErrors{field message}}}"""
    body=json.dumps({"query":q,"variables":{"m":mf}}).encode()
    req=urllib.request.Request(f"https://{SHOP}/admin/api/{VER}/graphql.json",data=body,
        headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
    with urllib.request.urlopen(req) as r: return json.load(r)

pg_n=0
for w in writes: pg_n+=pg_update(w)
print(f"  PG-first: {'applied' if APPLY else 'prepared'} {pg_n} metafield upserts to local mirror")

if APPLY:
    # Shopify in batches of 25 owners (<=~50 metafields/call safe)
    B=25; errs=0; done=0
    for i in range(0,len(writes),B):
        batch=writes[i:i+B]
        res=shopify_set(batch)
        ue=res.get("data",{}).get("metafieldsSet",{}).get("userErrors",[])
        if ue: errs+=len(ue); print("  SHOPIFY userErrors:",ue[:3])
        done+=len(batch); time.sleep(0.6)
    print(f"  Shopify: pushed {done} products, userErrors={errs}")
else:
    # show a sample metafieldsSet payload
    sample=writes[0]
    print("  sample Shopify metafieldsSet input for",sample["shopify_id"],":")
    for k,v in sample["fields"].items():
        ns,key=ns_key(k); print(f"    {ns}.{key} = {v!r} (single_line_text_field)")
    print("  DRY-RUN: no writes performed. Re-run with --apply after approval.")