[object Object]

← back to Designer Wallcoverings

deploy: push-min-qty-to-dev-theme.py — inject per-variant min-qty into a dev theme's theme.liquid (product-gated, backup, idempotent, THEME_ID override)

31e6eaee8685653af4fa37e30d5aad59b064a845 · 2026-06-29 11:45:23 -0700 · Steve Abrams

Files touched

Diff

commit 31e6eaee8685653af4fa37e30d5aad59b064a845
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 29 11:45:23 2026 -0700

    deploy: push-min-qty-to-dev-theme.py — inject per-variant min-qty into a dev theme's theme.liquid (product-gated, backup, idempotent, THEME_ID override)
---
 shopify/push-min-qty-to-dev-theme.py | 77 ++++++++++++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)

diff --git a/shopify/push-min-qty-to-dev-theme.py b/shopify/push-min-qty-to-dev-theme.py
new file mode 100644
index 00000000..1157c5fd
--- /dev/null
+++ b/shopify/push-min-qty-to-dev-theme.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+# Inject the per-variant min-quantity script into the DEV theme's layout/theme.liquid,
+# gated to product pages, idempotent (replaces any prior block between markers), with
+# a timestamped backup. Models push-variant-ux-to-dev-theme.sh. NEVER touches live.
+import sys, os, json, re, urllib.request, urllib.parse, datetime
+
+STORE = "designer-laboratory-sandbox.myshopify.com"
+API = "2024-10"
+SECRETS = os.path.expanduser("~/Projects/secrets-manager/.env")
+TARGET_NAME = "Updated copy of NewWall Template [Dev]"
+HERE = os.path.dirname(os.path.abspath(__file__))
+SNIPPET = os.path.join(HERE, "..", "DW-Websites", "variant-min-quantity.liquid")
+START, END = "<!-- DW min-qty start -->", "<!-- DW min-qty end -->"
+
+def token():
+    if os.environ.get("THEME_TOKEN"): return os.environ["THEME_TOKEN"]
+    for line in open(SECRETS, encoding="utf-8", errors="replace"):
+        if line.startswith("SHOPIFY_THEME_TOKEN="):
+            return line.split("=", 1)[1].strip().strip('"\'')
+    sys.exit("No theme token (SHOPIFY_THEME_TOKEN).")
+
+TOK = token()
+H = {"X-Shopify-Access-Token": TOK, "Content-Type": "application/json"}
+print("token …" + TOK[-4:])
+def get(url): return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=H)))
+
+# extract just the <script>…</script> from the snippet file
+src_snip = open(SNIPPET, encoding="utf-8").read()
+m = re.search(r"<script>.*?</script>", src_snip, re.S)
+if not m: sys.exit("no <script> in snippet")
+script = m.group(0)
+block = (f"\n{START}\n{{% if template contains 'product' %}}\n{script}\n{{% endif %}}\n{END}\n")
+
+themes = get(f"https://{STORE}/admin/api/{API}/themes.json")["themes"]
+want_id = os.environ.get("THEME_ID")
+if want_id:
+    match = [t for t in themes if str(t["id"]) == str(want_id)]
+else:
+    match = ([t for t in themes if t["name"] == TARGET_NAME]
+             or [t for t in themes if t["role"] == "development"])
+match = [t for t in match if t["role"] != "main"]   # never the live/main theme
+if not match: sys.exit("Target dev theme not found (live untouched).")
+theme = match[0]; tid = theme["id"]
+print(f'=== target: {tid} "{theme["name"]}" (role={theme["role"]}) — live untouched ===')
+
+key = "layout/theme.liquid"
+q = urllib.parse.urlencode({"asset[key]": key})
+cur = get(f"https://{STORE}/admin/api/{API}/themes/{tid}/assets.json?{q}")["asset"]["value"]
+
+# backup
+stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+bkdir = os.path.join(HERE, "theme-backups"); os.makedirs(bkdir, exist_ok=True)
+bk = os.path.join(bkdir, f"theme.liquid.minqty.{tid}.{stamp}.bak")
+open(bk, "w", encoding="utf-8").write(cur)
+print("backup:", bk, f"({len(cur)} bytes)")
+
+# remove any prior injected block (idempotent), then insert before </body>
+cleaned = re.sub(re.escape(START) + r".*?" + re.escape(END) + r"\n?", "", cur, flags=re.S)
+if "</body>" in cleaned:
+    newsrc = cleaned.replace("</body>", block + "</body>", 1)
+else:
+    newsrc = cleaned + block
+if newsrc == cur:
+    print("no change (already current)"); sys.exit(0)
+
+body = json.dumps({"asset": {"key": key, "value": newsrc}}).encode()
+req = urllib.request.Request(f"https://{STORE}/admin/api/{API}/themes/{tid}/assets.json",
+                            data=body, method="PUT", headers=H)
+try:
+    r = urllib.request.urlopen(req); print("PUT HTTP", r.status, f"OK ({len(newsrc)} bytes)")
+except urllib.error.HTTPError as e:
+    sys.exit(f"PUT FAILED HTTP {e.code}: {e.read().decode()[:300]}")
+
+ver = get(f"https://{STORE}/admin/api/{API}/themes/{tid}/assets.json?{q}")["asset"]["value"]
+print("verify: block present =", START in ver)
+print(f"\nPREVIEW: https://www.designerwallcoverings.com/products/novasuede%E2%84%A2-fabric-wallcovering-cement?variant=44218997211187&preview_theme_id={tid}")
+print(f"revert:  PUT {bk} back to {key} on theme {tid}")

← 9fcc5ff8 product min-qty: target real mm_ quantity widget + single-op  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-29T11:48:48 (5 files) — pending-approval/ 4f37afe2 →