← back to Hollywood Price 2026
quote_only.py
96 lines
#!/usr/bin/env python3
"""Quote-only the remaining unpriceable Hollywood products (Steve 2026-07-12).
After pricing (856) + archiving (267), the leftover ACTIVE + Needs-Price Hollywood
set is the ~246 unmatched + ~12 no-sku — no resolvable Momentum/Versa price
(Versa commercial publishes none). Mark them quote-only: ADD `quotes` tag, REMOVE
`Needs-Price`. Keeps active + Sample-only, no fabricated price. Reversible. $0."""
import sys
import os
import csv
import datetime
import time
sys.path.insert(0, "lib")
from shopify import gql
LEDGER = os.path.join("ledger", "quote_only.csv")
M_ADD = """mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id, tags:$tags){ userErrors{ message } } }"""
M_RM = """mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id, tags:$tags){ userErrors{ message } } }"""
Q = """query($q:String!,$after:String){ products(first:150, after:$after, query:$q){
pageInfo{hasNextPage endCursor}
edges{ node{ id legacyResourceId title tags
variants(first:5){ edges{ node{ sku title } } } } } } }"""
SEARCH = "vendor:'Hollywood Wallcoverings' status:active tag:Needs-Price"
def has_sellable(n):
for e in n["variants"]["edges"]:
v = e["node"]; sk = (v["sku"] or "")
if v["title"] != "Sample" and not sk.lower().endswith("-sample"):
return True
return False
def log(pid, title, status):
new = not (os.path.exists(LEDGER) and os.path.getsize(LEDGER) > 0)
with open(LEDGER, "a", newline="") as f:
w = csv.writer(f)
if new:
w.writerow(["ts", "product_id", "title", "status"])
w.writerow([datetime.datetime.now().isoformat(timespec="seconds"), pid, title, status])
def seen():
s = set()
if os.path.exists(LEDGER):
for r in csv.DictReader(open(LEDGER)):
if r["status"].startswith("OK") and r.get("product_id"):
s.add(r["product_id"])
return s
def fetch_all():
out, cur = [], None
while True:
d = gql(Q, {"q": SEARCH, "after": cur})
p = d["products"]; out += [e["node"] for e in p["edges"]]
if not p["pageInfo"]["hasNextPage"]:
break
cur = p["pageInfo"]["endCursor"]
return out
def process(n):
pid = str(n["legacyResourceId"]); gid = n["id"]
# safety: never quote-only a product that actually has a real sellable variant
if has_sellable(n):
log(pid, n["title"], "SKIP:has-sellable"); return "SKIP:has-sellable"
if "quotes" in n["tags"] and "Needs-Price" not in n["tags"]:
log(pid, n["title"], "SKIP:already-quote-only"); return "SKIP:already"
a = gql(M_ADD, {"id": gid, "tags": ["quotes"]})["tagsAdd"]["userErrors"]
r = gql(M_RM, {"id": gid, "tags": ["Needs-Price"]})["tagsRemove"]["userErrors"]
if a or r:
log(pid, n["title"], "FAIL:" + str(a or r)[:80]); return "FAIL"
log(pid, n["title"], "OK"); return "OK"
if __name__ == "__main__":
nodes = fetch_all()
print(f"remaining ACTIVE Needs-Price Hollywood: {len(nodes)}")
if "--canary" in sys.argv:
k = int(sys.argv[sys.argv.index("--canary") + 1])
for n in nodes[:k]:
print(f" {n['title'][:44]:44} -> {process(n)}")
sys.exit(0)
done = seen()
todo = [n for n in nodes if str(n["legacyResourceId"]) not in done]
ok = sk = fl = 0
for i, n in enumerate(todo):
st = process(n)
if st == "OK": ok += 1
elif st.startswith("FAIL"): fl += 1
else: sk += 1
if (i + 1) % 40 == 0:
print(f" ...{i+1}/{len(todo)} ok={ok} skip={sk} fail={fl}")
time.sleep(0.1)
print(f"QUOTE-ONLY DONE: ok={ok} skip={sk} fail={fl}")