[object Object]

← back to Dw Material Reclassify

auto-save: 2026-07-22T09:12:53 (3 files) — classify.py __pycache__/ apply_tags.py

5ef8ff19bfbe695e0f39f7fb4b37070ed3c962fc · 2026-07-22 09:13:00 -0700 · Steve Abrams

Files touched

Diff

commit 5ef8ff19bfbe695e0f39f7fb4b37070ed3c962fc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 09:13:00 2026 -0700

    auto-save: 2026-07-22T09:12:53 (3 files) — classify.py __pycache__/ apply_tags.py
---
 __pycache__/classify.cpython-314.pyc | Bin 0 -> 10562 bytes
 apply_tags.py                        |  94 +++++++++++++++++++++++++++++++++++
 classify.py                          |   6 +--
 3 files changed, 97 insertions(+), 3 deletions(-)

diff --git a/__pycache__/classify.cpython-314.pyc b/__pycache__/classify.cpython-314.pyc
new file mode 100644
index 0000000..3a51a81
Binary files /dev/null and b/__pycache__/classify.cpython-314.pyc differ
diff --git a/apply_tags.py b/apply_tags.py
new file mode 100644
index 0000000..3a581f9
--- /dev/null
+++ b/apply_tags.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""
+Wave-1 WRITE: apply a `material-group-<bucket>` tag to each FREE-resolved
+New-Arrival Wallcovering (tier1 Material: line + tier1.5 body statement).
+
+Boost SD filters on tags natively, so this tag becomes the Material facet source.
+Adding a tag is INVISIBLE on the storefront until the Boost filter is configured to
+use it, and is fully reversible (remove the tag).
+
+  python3 apply_tags.py                 # DRY RUN: print the write set + tag counts
+  python3 apply_tags.py --canary 3      # WRITE to first 3 products, then verify
+  python3 apply_tags.py --apply         # WRITE to all resolved products
+
+Idempotent: skips a product that already carries its material-group tag.
+Reads SHOPIFY_ADMIN_TOKEN from ~/Projects/secrets-manager/.env. No cost (Admin API
+writes are free).
+"""
+import os, re, sys, json, time, subprocess, urllib.request, urllib.error
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import classify as C
+
+DOM = "designer-laboratory-sandbox.myshopify.com"
+ENV = os.path.expanduser("~/Projects/secrets-manager/.env")
+
+def token():
+    for line in open(ENV):
+        if line.startswith("SHOPIFY_ADMIN_TOKEN="):
+            return line.split("=",1)[1].strip().strip('"').strip("'")
+    sys.exit("no SHOPIFY_ADMIN_TOKEN")
+
+def slug(bucket):
+    return "material-group-" + re.sub(r"[^a-z0-9]+","-",bucket.lower()).strip("-")
+
+def resolved_set():
+    """[(numeric_id, title, bucket, tag)] for FREE-resolved rows only."""
+    out=[]
+    for r in C.load_rows(scope_all=("--all" in sys.argv)):
+        tier,bucket,ev = C.classify(r)
+        if bucket and tier in ("tier1_material_line","tier15_body"):
+            gid=r.get("shopify_id") or ""
+            nid=gid.rsplit("/",1)[-1]
+            if nid.isdigit():
+                out.append((nid, r["title"], bucket, slug(bucket)))
+    return out
+
+def api(method, path, tok, body=None):
+    req=urllib.request.Request(f"https://{DOM}/admin/api/2024-10/{path}",
+        method=method, headers={"X-Shopify-Access-Token":tok,"Content-Type":"application/json"},
+        data=json.dumps(body).encode() if body else None)
+    return json.load(urllib.request.urlopen(req))
+
+def apply_one(nid, tag, tok):
+    p=api("GET", f"products/{nid}.json?fields=id,tags", tok)["product"]
+    tags=[t.strip() for t in (p.get("tags") or "").split(",") if t.strip()]
+    if tag in tags:
+        return "skip(present)"
+    tags.append(tag)
+    api("PUT", f"products/{nid}.json", tok, {"product":{"id":int(nid),"tags":", ".join(tags)}})
+    # verify
+    v=api("GET", f"products/{nid}.json?fields=id,tags", tok)["product"]
+    return "OK" if tag in (v.get("tags") or "") else "FAIL(verify)"
+
+def main():
+    rows=resolved_set()
+    from collections import Counter
+    by_tag=Counter(t for _,_,_,t in rows)
+    print(f"Wave-1 tag write set: {len(rows)} products")
+    for t,n in by_tag.most_common(): print(f"  {t:28s} {n}")
+
+    canary = None
+    if "--canary" in sys.argv:
+        canary = int(sys.argv[sys.argv.index("--canary")+1])
+    do_write = ("--apply" in sys.argv) or (canary is not None)
+    if not do_write:
+        print("\nDRY RUN — no writes. Re-run with --canary N or --apply."); return
+
+    tok=token()
+    target = rows[:canary] if canary is not None else rows
+    print(f"\nWRITING to {len(target)} products (canary={canary})...")
+    res=Counter()
+    for nid,title,bucket,tag in target:
+        try:
+            r=apply_one(nid,tag,tok)
+        except urllib.error.HTTPError as e:
+            r=f"HTTP{e.code}"
+        res[r]+=1
+        if canary is not None:
+            print(f"  {r:16s} {tag:26s} {title[:40]}")
+        time.sleep(0.3)  # gentle on the 2 req/s REST limit
+    print("result:", dict(res))
+    print("Cost: $0 (Admin API writes are free).")
+
+if __name__=="__main__":
+    main()
diff --git a/classify.py b/classify.py
index 488ab10..e8331b3 100644
--- a/classify.py
+++ b/classify.py
@@ -84,8 +84,8 @@ def build_sql(scope_all=False):
 
 SQL_TMPL = r"""
 with wc as (
-  select id, title, coalesce(vendor,'') vendor, coalesce(dw_sku,'') dw_sku,
-         coalesce(pattern_name,'') pattern_name,
+  select id, coalesce(shopify_id,'') shopify_id, title, coalesce(vendor,'') vendor,
+         coalesce(dw_sku,'') dw_sku, coalesce(pattern_name,'') pattern_name,
          coalesce(body_html,'') body_html,
          lower(regexp_replace(tags,'color:[a-z ]+','','g')) as t
   from shopify_products
@@ -93,7 +93,7 @@ with wc as (
     __NA__ and tags is not null
 )
 select json_agg(json_build_object(
-  'id',id,'title',title,'vendor',vendor,'dw_sku',dw_sku,
+  'id',id,'shopify_id',shopify_id,'title',title,'vendor',vendor,'dw_sku',dw_sku,
   'pattern_name',pattern_name,'body_html',body_html))
 from wc
 where not (t ~ '(cork|grasscloth|paperweave|paper weave|sisal|jute|hemp|abaca|raffia|seagrass|arrowroot|silk|linen|leather|glass bead|beaded|flock|metallic|mica|mylar|foil|tedlar|wood veneer|veneer|paulownia|vinyl|type 2|type ii|20 oz|natural wallcovering|naturals|natural fiber|natural textile|natural resource)')

← a3d8f0a Add --all full-catalog scope; full run = 13% free / 9456 vis  ·  back to Dw Material Reclassify  ·  gitignore __pycache__ 5a9fa69 →