← back to Kravet Sheet Sync 2026 04 20
fix_audit_issues.py
203 lines
#!/usr/bin/env python3
"""
Fix issues from audit_last_30:
A) Add 5 active products missing from New Arrivals collection
B) Fix 3 DRAFT products with FAILED images — upload from FTP mirror via
stagedUploadsCreate, then attach and re-activate
"""
import json, os, subprocess, urllib.request, urllib.parse
TOKEN=os.environ.get('SHOPIFY_ADMIN_TOKEN','')
SHOP='designer-laboratory-sandbox.myshopify.com'
URL=f'https://{SHOP}/admin/api/2024-10/graphql.json'
NEW_ARRIVALS='gid://shopify/Collection/167327760435'
SSH=['ssh','root@45.61.58.125']
MISSING_COLLECTION_ACTIVE = [
"gid://shopify/Product/7821710032947", # DWKK-142909 Tall Trees G P & J Baker
"gid://shopify/Product/7821710065715", # DWKK-143582 Songbird Dawn
"gid://shopify/Product/7821710131251", # DWKK-144813 Mandalay Sisal Willow
"gid://shopify/Product/7821740605491", # DWKK-155345 Fallingwater Charcoal Threads
]
IMAGE_FAILED = [
# DWKK, product_gid, mfr_sku, remote FTP path, local Kamatera mirror path
{"dw_sku":"DWKK-155119","pid":"gid://shopify/Product/7821740539955",
"mfr":"FG125.S110.0","title":"Grand Mulberry Woodland Summer | Mulberry",
"ftp_path":"/KF/HIRES/FG125_S110.JPG"},
{"dw_sku":"DWKK-155304","pid":"gid://shopify/Product/7821740572723",
"mfr":"NZAW112026.SCN.0","title":"Parlour Palm | Scion",
"ftp_path":"/KF/HIRES/NZAW112026_SCN.JPG"},
{"dw_sku":"DWKK-156194","pid":"gid://shopify/Product/7821740638259",
"mfr":"WHF3846.WT.0","title":"Compass Clay | Winfield Thybony",
"ftp_path":"/KF/HIRES/WHF3846_WT.JPG"},
]
def gql(q, v=None):
body=json.dumps({"query":q,"variables":v or {}}).encode()
req=urllib.request.Request(URL,data=body,method="POST",
headers={"Content-Type":"application/json","X-Shopify-Access-Token":TOKEN})
d=json.loads(urllib.request.urlopen(req,timeout=30).read())
if "errors" in d: raise RuntimeError(d["errors"])
return d["data"]
# ============================================================
# A) Add missing products to New Arrivals collection
# ============================================================
print("=== A) Adding missing products to New Arrivals ===")
COL_ADD = """mutation($id:ID!,$pids:[ID!]!){collectionAddProducts(id:$id,productIds:$pids){userErrors{field message}}}"""
d = gql(COL_ADD, {"id": NEW_ARRIVALS, "pids": MISSING_COLLECTION_ACTIVE})
errs = d["collectionAddProducts"]["userErrors"]
print(f" added {len(MISSING_COLLECTION_ACTIVE)} products to New Arrivals" + (f" (errors: {errs})" if errs else ""))
# ============================================================
# B) Fix 3 DRAFT products with failed images
# ============================================================
print("\n=== B) Fixing DRAFT products with failed Brandfolder fetches ===")
# Check what's on the FTP mirror for each — verify file exists
for item in IMAGE_FAILED:
r = subprocess.run(SSH + [f"ls -la /root/kravet-ftp-mirror{item['ftp_path']} 2>&1 || echo MISSING"],
capture_output=True, text=True)
item["ftp_exists"] = "MISSING" not in r.stdout
print(f" {item['dw_sku']} ftp {item['ftp_path']}: {'✓' if item['ftp_exists'] else '✗ MISSING'}")
# For each missing file, try finding actual filename
# Look for alt brandfolder URL in kravet_sku_images
import io, csv
for item in IMAGE_FAILED:
if item["ftp_exists"]:
continue
esc_sku = item["mfr"].replace("'", "''")
r = subprocess.run(SSH + [
f'PGPASSWORD=DW2024! psql -h 127.0.0.1 -U dw_admin -d dw_unified --csv -t -c '
f'"SELECT url FROM kravet_sku_images WHERE mfr_sku_raw = \'{esc_sku}\' ORDER BY kind, url LIMIT 3"'],
capture_output=True, text=True)
urls = [r[0] for r in csv.reader(io.StringIO(r.stdout)) if r]
print(f" {item['dw_sku']} fallback URLs from catalog ({len(urls)}):")
for u in urls: print(f" {u[:130]}")
item["fallback_urls"] = urls
# Upload an image for each missing product via stagedUploadsCreate
STAGED_UP = """
mutation($input:[StagedUploadInput!]!){
stagedUploadsCreate(input:$input){
stagedTargets{ url resourceUrl parameters{ name value } }
userErrors{ field message }
}
}"""
MEDIA_CREATE = """
mutation($pid:ID!,$m:[CreateMediaInput!]!){
productCreateMedia(productId:$pid,media:$m){
media{ ... on MediaImage{ id status } }
mediaUserErrors{ field message }
}
}"""
MEDIA_DEL = """
mutation($pid:ID!,$ids:[ID!]!){
productDeleteMedia(productId:$pid,mediaIds:$ids){ deletedMediaIds mediaUserErrors{ field message } }
}"""
PROD_UPD = """
mutation($p:ProductUpdateInput!){productUpdate(product:$p){product{id status} userErrors{field message}}}"""
def fetch_from_kamatera(remote_path, local_path):
"""SCP a single file from Kamatera mirror to local."""
r = subprocess.run(["scp", f"root@45.61.58.125:{remote_path}", local_path],
capture_output=True, text=True)
return r.returncode == 0 and os.path.exists(local_path) and os.path.getsize(local_path) > 0
def staged_upload_shopify(local_path, mime="image/jpeg"):
fsize = os.path.getsize(local_path)
fname = os.path.basename(local_path)
d = gql(STAGED_UP, {"input": [{
"filename": fname, "mimeType": mime, "resource": "IMAGE",
"httpMethod": "POST", "fileSize": str(fsize),
}]})
errs = d["stagedUploadsCreate"]["userErrors"]
if errs: raise RuntimeError(f"staged create: {errs}")
target = d["stagedUploadsCreate"]["stagedTargets"][0]
# Build multipart manually
boundary = "----dw-form-" + os.urandom(8).hex()
body_parts = []
for p in target["parameters"]:
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{p["name"]}"\r\n\r\n{p["value"]}\r\n')
# file part
with open(local_path, "rb") as f: file_bytes = f.read()
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{fname}"\r\nContent-Type: {mime}\r\n\r\n')
body = "".join(body_parts).encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(target["url"], data=body, method="POST",
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
urllib.request.urlopen(req, timeout=60).read()
return target["resourceUrl"]
import os
TMP_DIR = "/tmp/kravet_fallback_images"
os.makedirs(TMP_DIR, exist_ok=True)
for item in IMAGE_FAILED:
dw_sku, pid = item["dw_sku"], item["pid"]
print(f"\n [{dw_sku}] {item['title']}")
# 1) Delete any old failed media
cur = gql("query($id:ID!){product(id:$id){media(first:10){nodes{id}}}}", {"id": pid})
media_ids = [m["id"] for m in cur["product"]["media"]["nodes"]]
if media_ids:
gql(MEDIA_DEL, {"pid": pid, "ids": media_ids})
print(f" deleted {len(media_ids)} failed media")
# 2) Get image source
local = None
if item.get("ftp_exists"):
local = f"{TMP_DIR}/{dw_sku}.jpg"
if fetch_from_kamatera(f"/root/kravet-ftp-mirror{item['ftp_path']}", local):
print(f" pulled from FTP mirror: {os.path.getsize(local):,} bytes")
else:
local = None
if not local:
# Try catalog fallback URLs
for u in item.get("fallback_urls", []):
try:
req = urllib.request.Request(u.replace(" ", "%20"),
headers={"User-Agent":"Mozilla/5.0"})
b = urllib.request.urlopen(req, timeout=30).read()
if len(b) > 20000: # real image, not placeholder
local = f"{TMP_DIR}/{dw_sku}.jpg"
open(local, "wb").write(b)
print(f" pulled from URL: {len(b):,} bytes")
break
except Exception as e:
print(f" URL fail: {e}")
if not local:
print(f" SKIP: no image source")
continue
# 3) Staged upload + attach
try:
resource_url = staged_upload_shopify(local)
print(f" staged → {resource_url[:80]}")
r = gql(MEDIA_CREATE, {"pid": pid, "m": [{
"originalSource": resource_url,
"mediaContentType": "IMAGE",
"alt": item["title"],
}]})
errs = r["productCreateMedia"]["mediaUserErrors"]
if errs:
print(f" attach errors: {errs}")
else:
m = r["productCreateMedia"]["media"][0]
print(f" attached: {m['id']} status={m['status']}")
except Exception as e:
print(f" stage/attach fail: {e}")
print("\ndone.")