← back to Dw Photo Capture

refresh_mfr_index.py

80 lines

#!/usr/bin/env python3
"""Incremental refresh of data/mfr_index.json — pulls ONLY products changed since the
last build/refresh (updated_at filter) and merges them in. Runs in seconds, $0, so it
can fire hourly during work hours to keep same-day-imported SKUs scannable. The nightly
full build_mfr_index.py is the backstop that also clears stale/deleted keys."""
import json, os, re, sys, time, urllib.request
from datetime import datetime, timezone, timedelta

SHOP = "designer-laboratory-sandbox.myshopify.com"; API = "2024-10"
ROOT = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(ROOT, "data", "mfr_index.json")
TOKEN = os.environ.get("SHOPIFY_ADMIN_TOKEN", "")
if not TOKEN:
    for line in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
        if line.startswith("SHOPIFY_ADMIN_TOKEN="):
            TOKEN = line.strip().split("=", 1)[1]; break
assert TOKEN, "no SHOPIFY_ADMIN_TOKEN"

def gql(query, variables=None):
    req = urllib.request.Request(f"https://{SHOP}/admin/api/{API}/graphql.json",
        data=json.dumps({"query": query, "variables": variables or {}}).encode(),
        headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req, timeout=90))

def norm(s):  return re.sub(r'[^A-Z0-9]', '', (s or '').upper())
def parts(v): return [p for p in re.split(r'[;,/|]+', v or '') if p.strip()]
def house_sku(skus):
    real = [s for s in skus if s and not s.lower().endswith("-sample")]
    return (real or [s for s in skus if s] or [""])[0]

if not os.path.exists(OUT):
    print("no index yet — run build_mfr_index.py first (full build)"); sys.exit(1)
idx = json.load(open(OUT))
meta = idx.get("_meta", {})
# baseline: last built_at, else the file's mtime, minus a 10-min safety overlap
since_iso = meta.get("built_at")
if not since_iso:
    since_iso = datetime.fromtimestamp(os.path.getmtime(OUT), tz=timezone.utc).isoformat()
since = datetime.fromisoformat(since_iso) - timedelta(minutes=10)
since_q = since.strftime("%Y-%m-%dT%H:%M:%SZ")
print(f"refreshing products updated since {since_q}")

Q = '''query($c:String,$q:String){ products(first:100, query:$q, after:$c){
  pageInfo{hasNextPage endCursor}
  edges{node{ id handle status title
    mf: metafield(namespace:"custom",key:"manufacturer_sku"){value}
    mf2: metafield(namespace:"dwc",key:"manufacturer_sku"){value}
    variants(first:10){edges{node{sku}}} }} } }'''
cur = None; changed = 0; pages = 0
while True:
    g = gql(Q, {"c": cur, "q": f"updated_at:>='{since_q}'"})
    d = g.get("data", {}).get("products")
    if d is None:
        print("GQL err:", json.dumps(g)[:300]); sys.exit(1)
    for e in d["edges"]:
        n = e["node"]
        sku = house_sku([v["node"]["sku"] for v in n["variants"]["edges"]])
        if not sku: continue
        keys = set()
        for k in ("mf", "mf2"):
            for m in parts((n.get(k) or {}).get("value")):
                if len(norm(m)) >= 4: keys.add(norm(m))
        if len(norm(sku)) >= 4: keys.add(norm(sku))
        rec = {"sku": sku, "title": (n.get("title") or "")[:80], "status": n.get("status"), "handle": n.get("handle")}
        for kk in keys: idx[kk] = rec
        changed += 1
    pages += 1
    if d["pageInfo"]["hasNextPage"] and pages < 80:
        cur = d["pageInfo"]["endCursor"]; time.sleep(0.3)
    else:
        if d["pageInfo"]["hasNextPage"]: print("⚠ stopped at 80 pages — nightly full build will reconcile")
        break

idx["_meta"] = {**meta, "keys": sum(1 for k in idx if k != "_meta"),
                "built_at": datetime.now(timezone.utc).isoformat(),
                "last_incremental": datetime.now(timezone.utc).isoformat(),
                "last_incremental_changed": changed}
json.dump(idx, open(OUT, "w"))
print(f"merged {changed} changed products across {pages} page(s) -> {idx['_meta']['keys']} keys")