Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling
A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.
What is here:
storefront/ the Next.js app
backend/ ONLY the custom Spree code. Spree itself is a prebuilt image
that mounts no app code, so every file here needs an explicit
volume entry in docker-compose.yml or it is silently ignored.
catalogue/ the scripts that loaded and maintain the products
docs/ manifest, mock registry, napkins
Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.
The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Spree ADMIN API key — full write access to products, prices and orders.
|
||||
# The scripts here read it from the environment and refuse to run without it;
|
||||
# it used to be a literal in six files, which is why this exists.
|
||||
#
|
||||
# export SPREE_ADMIN_KEY=$(grep SPREE_ADMIN_KEY catalogue/.env | cut -d= -f2)
|
||||
SPREE_ADMIN_KEY=sk_ntDFxFE1KwiDwqGzpCBGTJ8N
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pull a full Pak Jabit backend backup to the house.
|
||||
# - pg_dump (custom format) of spree_production
|
||||
# - ActiveStorage volume, rsync'd mirror (~208MB, only deltas after the first run)
|
||||
# - .env (SECRET_KEY_BASE lives here — without it every signed image URL and
|
||||
# the admin API token die on restore)
|
||||
#
|
||||
# Usage: backup-pak-jabit.sh # normal run
|
||||
# backup-pak-jabit.sh --verify # also list the dump's table counts
|
||||
set -euo pipefail
|
||||
|
||||
HOST=ijat@35.247.170.3
|
||||
KEY=/root/.ssh/google_compute_engine
|
||||
SSH=(ssh -i "$KEY" -o ConnectTimeout=15 -o BatchMode=yes)
|
||||
DEST=/root/Backups/pak-jabit
|
||||
KEEP=14 # dated dumps to retain
|
||||
STAMP=$(date +%Y%m%d-%H%M)
|
||||
|
||||
STORAGE_REMOTE=/var/lib/docker/volumes/pak-jabit_storage_data/_data/
|
||||
mkdir -p "$DEST"/{db,storage}
|
||||
chmod 700 "$DEST"
|
||||
|
||||
log() { printf '%s %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
|
||||
# --- 1. database -------------------------------------------------------------
|
||||
log "pg_dump → db/spree-$STAMP.dump"
|
||||
"${SSH[@]}" "$HOST" \
|
||||
'docker exec pak-jabit-postgres-1 pg_dump -U postgres -Fc spree_production' \
|
||||
> "$DEST/db/spree-$STAMP.dump.part"
|
||||
mv "$DEST/db/spree-$STAMP.dump.part" "$DEST/db/spree-$STAMP.dump"
|
||||
|
||||
# A truncated dump is the classic silent-success backup. Refuse to keep one.
|
||||
if ! pg_restore -l "$DEST/db/spree-$STAMP.dump" >/dev/null 2>&1; then
|
||||
rm -f "$DEST/db/spree-$STAMP.dump"
|
||||
echo "FATAL: dump failed pg_restore -l — not a usable archive, deleted." >&2
|
||||
exit 1
|
||||
fi
|
||||
log " ok $(du -h "$DEST/db/spree-$STAMP.dump" | cut -f1)"
|
||||
|
||||
# --- 2. ActiveStorage --------------------------------------------------------
|
||||
log "rsync storage/"
|
||||
rsync -a --delete --info=stats2 \
|
||||
-e "ssh -i $KEY -o BatchMode=yes" \
|
||||
--rsync-path='sudo rsync' \
|
||||
"$HOST:$STORAGE_REMOTE" "$DEST/storage/" | tail -n 6
|
||||
|
||||
# --- 3. secrets & compose ----------------------------------------------------
|
||||
log "env + compose"
|
||||
"${SSH[@]}" "$HOST" 'cat ~/pak-jabit/.env' > "$DEST/env-$STAMP"
|
||||
"${SSH[@]}" "$HOST" 'cat ~/pak-jabit/docker-compose.yml' > "$DEST/docker-compose.yml"
|
||||
chmod 600 "$DEST"/env-*
|
||||
ln -sfn "env-$STAMP" "$DEST/env.latest"
|
||||
|
||||
# --- 3b. custom app code -----------------------------------------------------
|
||||
# The Spree image is prebuilt and mounts no app code, so every customisation
|
||||
# (Image Studio, the API body-limit initializer, the patched routes.rb) exists
|
||||
# ONLY as bind-mounted files on this VM. The database dump does not contain them
|
||||
# and a rebuilt box would come back without them.
|
||||
log "custom code"
|
||||
rsync -a --delete \
|
||||
-e "ssh -i $KEY -o BatchMode=yes" \
|
||||
--include='config/' --include='config/routes.rb' \
|
||||
--include='config/initializers/***' \
|
||||
--include='app/' --include='app/controllers/***' --include='app/views/***' \
|
||||
--exclude='*' \
|
||||
"$HOST:pak-jabit/" "$DEST/custom/"
|
||||
find "$DEST/custom" -type f | sed "s|$DEST/custom/| |"
|
||||
|
||||
# --- 4. rotate ---------------------------------------------------------------
|
||||
ls -1t "$DEST"/db/spree-*.dump 2>/dev/null | tail -n +$((KEEP + 1)) | xargs -r rm -v
|
||||
ls -1t "$DEST"/env-* 2>/dev/null | tail -n +$((KEEP + 1)) | xargs -r rm -v
|
||||
|
||||
if [[ ${1:-} == --verify ]]; then
|
||||
log "dump contents:"
|
||||
pg_restore -l "$DEST/db/spree-$STAMP.dump" | grep -c 'TABLE DATA' \
|
||||
| xargs printf ' %s tables with data\n'
|
||||
fi
|
||||
|
||||
log "done → $DEST ($(du -sh "$DEST" | cut -f1) total)"
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pak Jabit Store — Bulk Product Import via Spree Admin API v3
|
||||
|
||||
Creates products, uploads images, publishes to Online Store channel,
|
||||
and sets stock — all through the official Admin API.
|
||||
|
||||
Usage:
|
||||
python3 bulk_import.py [--dry-run] [--start N] [--limit N]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
# --- Config ---
|
||||
API_BASE = "https://api.jux-lab.com/api/v3/admin"
|
||||
API_KEY = _spree_admin_key()
|
||||
HEADERS = {"X-Spree-Api-Key": API_KEY, "Content-Type": "application/json"}
|
||||
CHANNEL_ID = "ch_UkLWZg9DAJ" # Online Store
|
||||
MYR_TO_USD = 4.8
|
||||
PUBLISHED_AT = "2026-09-15T00:00:00Z"
|
||||
|
||||
CATALOGUE_DIR = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue")
|
||||
PRODUCTS_DIR = CATALOGUE_DIR / "products"
|
||||
BATCH_FILES = ["products-batch2.jsonl", "products-batch3.jsonl"]
|
||||
|
||||
LOG_FILE = CATALOGUE_DIR / "import_log.jsonl"
|
||||
|
||||
|
||||
def api(method, path, **kwargs):
|
||||
url = f"{API_BASE}{path}"
|
||||
headers = dict(HEADERS)
|
||||
if "files" in kwargs:
|
||||
headers.pop("Content-Type", None)
|
||||
r = getattr(requests, method)(url, headers=headers, timeout=60, **kwargs)
|
||||
try:
|
||||
return r.json()
|
||||
except Exception:
|
||||
return {"error": {"code": r.status_code, "message": r.text[:200]}}
|
||||
|
||||
|
||||
def find_product_folder(code, name):
|
||||
"""Match JSONL entry to local image folder."""
|
||||
for folder in sorted(PRODUCTS_DIR.iterdir()):
|
||||
info_file = folder / "info.json"
|
||||
if info_file.exists():
|
||||
info = json.loads(info_file.read_text())
|
||||
if info.get("source_code") == code:
|
||||
return folder
|
||||
return None
|
||||
|
||||
|
||||
def create_product(name, brand, price_myr, slug=None):
|
||||
price = round(float(price_myr) * MYR_TO_USD, 2)
|
||||
desc = f"{brand} - {name}"
|
||||
# A scalar "price" is silently dropped; Spree wants a prices array, and a
|
||||
# product with no price row is excluded from the storefront's
|
||||
# available(currency) scope no matter how it is published.
|
||||
payload = {
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"prices": [{"amount": price, "currency": "USD"}],
|
||||
"status": "active",
|
||||
"available_on": "2026-09-15T00:00:00Z",
|
||||
}
|
||||
if slug:
|
||||
payload["slug"] = slug
|
||||
return api("post", "/products", json=payload)
|
||||
|
||||
|
||||
def upload_image(product_id, variant_id, image_path):
|
||||
with open(image_path, "rb") as f:
|
||||
return api("post",
|
||||
f"/products/{product_id}/variants/{variant_id}/media",
|
||||
files={"attachment": (image_path.name, f, "image/jpeg")})
|
||||
|
||||
|
||||
def publish_to_channel(product_id):
|
||||
# Without published_at the publication row exists but is not live.
|
||||
return api("post",
|
||||
f"/channels/{CHANNEL_ID}/add_products",
|
||||
json={"product_ids": [product_id], "published_at": PUBLISHED_AT})
|
||||
|
||||
|
||||
def make_purchasable(product_id, variant_id):
|
||||
return api("patch", f"/products/{product_id}/variants/{variant_id}",
|
||||
json={"track_inventory": False})
|
||||
|
||||
|
||||
def log_result(entry):
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
|
||||
def get_existing_slugs():
|
||||
slugs = set()
|
||||
page = 1
|
||||
while True:
|
||||
data = api("get", f"/products?per_page=100&page={page}")
|
||||
products = data.get("data", [])
|
||||
for p in products:
|
||||
slugs.add(p.get("slug", ""))
|
||||
meta = data.get("meta", {})
|
||||
if page >= meta.get("pages", 1):
|
||||
break
|
||||
page += 1
|
||||
return slugs
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--start", type=int, default=0)
|
||||
parser.add_argument("--limit", type=int, default=999)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load all products from batch files
|
||||
products = []
|
||||
for batch_file in BATCH_FILES:
|
||||
path = CATALOGUE_DIR / batch_file
|
||||
if path.exists():
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
products.append(json.loads(line.strip()))
|
||||
|
||||
print(f"Loaded {len(products)} products from {len(BATCH_FILES)} batch files")
|
||||
|
||||
# Get existing to skip duplicates
|
||||
print("Fetching existing products...")
|
||||
existing = get_existing_slugs()
|
||||
print(f" {len(existing)} products already in store")
|
||||
|
||||
# Process
|
||||
imported = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
products_slice = products[args.start:args.start + args.limit]
|
||||
|
||||
for i, prod in enumerate(products_slice):
|
||||
idx = args.start + i
|
||||
name = prod["name"]
|
||||
brand = prod["brand"]
|
||||
code = prod["code"]
|
||||
price = prod["price"]
|
||||
|
||||
folder = find_product_folder(code, name)
|
||||
if not folder:
|
||||
print(f" [{idx+1}] SKIP (no folder): {brand} - {name}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Check slug collision
|
||||
info = json.loads((folder / "info.json").read_text())
|
||||
slug = folder.name
|
||||
|
||||
if slug in existing:
|
||||
print(f" [{idx+1}] SKIP (exists): {slug}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if args.dry_run:
|
||||
images = list(folder.glob("image-*.jpg"))
|
||||
print(f" [{idx+1}] DRY: {brand} - {name} ({len(images)} imgs, ${float(price)*MYR_TO_USD:.2f})")
|
||||
continue
|
||||
|
||||
# 1. Create product
|
||||
result = create_product(name, brand, price, slug)
|
||||
if "error" in result:
|
||||
print(f" [{idx+1}] FAIL create: {result['error']}")
|
||||
log_result({"code": code, "action": "create", "error": result["error"]})
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
product_id = result["id"]
|
||||
variant_id = result.get("default_variant_id", "")
|
||||
print(f" [{idx+1}] Created: {product_id} — {brand} - {name}")
|
||||
|
||||
# 2. Upload images
|
||||
images = sorted(folder.glob("image-*.jpg"))
|
||||
img_ok = 0
|
||||
for img in images:
|
||||
r = upload_image(product_id, variant_id, img)
|
||||
if "error" not in r:
|
||||
img_ok += 1
|
||||
else:
|
||||
print(f" WARN image {img.name}: {r['error']}")
|
||||
time.sleep(0.3)
|
||||
print(f" Images: {img_ok}/{len(images)}")
|
||||
|
||||
# 3. Publish to Online Store
|
||||
pub = publish_to_channel(product_id)
|
||||
if "error" in pub:
|
||||
print(f" WARN publish: {pub['error']}")
|
||||
|
||||
# 4. Make purchasable
|
||||
stk = make_purchasable(product_id, variant_id)
|
||||
if "error" in stk:
|
||||
print(f" WARN stock: {stk['error']}")
|
||||
|
||||
log_result({
|
||||
"code": code, "product_id": product_id, "slug": slug,
|
||||
"brand": brand, "name": name, "images": img_ok,
|
||||
"price_usd": round(float(price) * MYR_TO_USD, 2)
|
||||
})
|
||||
|
||||
imported += 1
|
||||
existing.add(slug)
|
||||
time.sleep(0.5)
|
||||
|
||||
print(f"\nDone! Imported: {imported}, Skipped: {skipped}, Failed: {failed}")
|
||||
if not args.dry_run and imported > 0:
|
||||
print(f"Log: {LOG_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
import json, os, re, urllib.request, time
|
||||
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
out_root = os.path.join(base, "products")
|
||||
os.makedirs(out_root, exist_ok=True)
|
||||
|
||||
def slugify(name):
|
||||
s = re.sub(r'[^a-zA-Z0-9]+', '-', name).strip('-').lower()
|
||||
return s[:60]
|
||||
|
||||
catalogue = []
|
||||
with open(os.path.join(base, "products.jsonl")) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
catalogue.append(json.loads(line))
|
||||
|
||||
for i, p in enumerate(catalogue, 1):
|
||||
dirname = f"{i:02d}-{slugify(p['brand'])}-{slugify(p['name'])}"
|
||||
pdir = os.path.join(out_root, dirname)
|
||||
os.makedirs(pdir, exist_ok=True)
|
||||
with open(os.path.join(pdir, "info.json"), "w") as f:
|
||||
json.dump({"name": p["name"], "brand": p["brand"], "price_myr": p["price"], "source_code": p["code"]}, f, indent=2)
|
||||
for j, url in enumerate(p["imgs"], 1):
|
||||
dest = os.path.join(pdir, f"image-{j}.jpg")
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r, open(dest, "wb") as out:
|
||||
out.write(r.read())
|
||||
except Exception as e:
|
||||
print(f"FAIL {url}: {e}")
|
||||
print(f"[{i}/{len(catalogue)}] {p['brand']} - {p['name']} -> {len(p['imgs'])} imgs")
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download images for batch2 + batch3 products."""
|
||||
import json, os, re, urllib.request, time, sys
|
||||
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
out_root = os.path.join(base, "products")
|
||||
os.makedirs(out_root, exist_ok=True)
|
||||
|
||||
def slugify(name):
|
||||
s = re.sub(r'[^a-zA-Z0-9]+', '-', name).strip('-').lower()
|
||||
return s[:60]
|
||||
|
||||
def load_jsonl(path):
|
||||
items = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
items.append(json.loads(line))
|
||||
return items
|
||||
|
||||
existing = load_jsonl(os.path.join(base, "products.jsonl"))
|
||||
offset = len(existing)
|
||||
|
||||
batches = []
|
||||
for fname in ["products-batch2.jsonl", "products-batch3.jsonl"]:
|
||||
path = os.path.join(base, fname)
|
||||
if os.path.exists(path):
|
||||
batches.extend(load_jsonl(path))
|
||||
|
||||
print(f"Existing: {offset} products, new: {len(batches)} to download")
|
||||
|
||||
ok, fail = 0, 0
|
||||
for i, p in enumerate(batches, 1):
|
||||
idx = offset + i
|
||||
dirname = f"{idx:03d}-{slugify(p['brand'])}-{slugify(p['name'])}"
|
||||
pdir = os.path.join(out_root, dirname)
|
||||
os.makedirs(pdir, exist_ok=True)
|
||||
|
||||
info = {"name": p["name"], "brand": p["brand"], "price_myr": p["price"], "source_code": p["code"]}
|
||||
if "category" in p:
|
||||
info["category"] = p["category"]
|
||||
with open(os.path.join(pdir, "info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
img_ok = 0
|
||||
for j, url in enumerate(p["imgs"], 1):
|
||||
dest = os.path.join(pdir, f"image-{j}.jpg")
|
||||
if os.path.exists(dest) and os.path.getsize(dest) > 1000:
|
||||
img_ok += 1
|
||||
continue
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r, open(dest, "wb") as out:
|
||||
out.write(r.read())
|
||||
img_ok += 1
|
||||
except Exception as e:
|
||||
print(f" FAIL {url}: {e}")
|
||||
fail += 1
|
||||
time.sleep(0.2)
|
||||
|
||||
ok += img_ok
|
||||
print(f"[{i}/{len(batches)}] {p['brand']} - {p['name'][:40]} -> {img_ok}/{len(p['imgs'])} imgs")
|
||||
|
||||
print(f"\nDone: {ok} images downloaded, {fail} failed")
|
||||
@@ -0,0 +1,57 @@
|
||||
import json, time, requests
|
||||
from pathlib import Path
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
API = "https://api.jux-lab.com/api/v3/admin"
|
||||
H = {"X-Spree-Api-Key": _spree_admin_key(), "Content-Type": "application/json"}
|
||||
PRODUCTS = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products")
|
||||
MYR_TO_USD = 4.8
|
||||
|
||||
prices = {}
|
||||
for folder in PRODUCTS.iterdir():
|
||||
info = folder / "info.json"
|
||||
if info.exists():
|
||||
prices[folder.name] = round(float(json.loads(info.read_text())["price_myr"]) * MYR_TO_USD, 2)
|
||||
|
||||
items, page = [], 1
|
||||
while True:
|
||||
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
|
||||
items += d.get("data", [])
|
||||
if page >= d.get("meta", {}).get("pages", 1):
|
||||
break
|
||||
page += 1
|
||||
|
||||
print(f"{len(items)} products, {len(prices)} source prices")
|
||||
ok = fail = 0
|
||||
for i, p in enumerate(items):
|
||||
amt = prices.get(p["slug"])
|
||||
if amt is None:
|
||||
print(f" no source price: {p['slug'][:45]}")
|
||||
fail += 1
|
||||
continue
|
||||
r = requests.patch(f"{API}/products/{p['id']}", headers=H,
|
||||
json={"prices": [{"amount": amt, "currency": "USD"}]}, timeout=60).json()
|
||||
if "error" in r:
|
||||
fail += 1
|
||||
print(f" FAIL {p['slug'][:40]}: {r['error'].get('message')}")
|
||||
else:
|
||||
ok += 1
|
||||
if (i + 1) % 40 == 0:
|
||||
print(f" {i+1}/{len(items)}")
|
||||
time.sleep(0.12)
|
||||
|
||||
print(f"Done ok={ok} fail={fail}")
|
||||
@@ -0,0 +1,48 @@
|
||||
import json, sys, time, requests
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
API = "https://api.jux-lab.com/api/v3/admin"
|
||||
KEY = _spree_admin_key()
|
||||
H = {"X-Spree-Api-Key": KEY, "Content-Type": "application/json"}
|
||||
|
||||
products, page = [], 1
|
||||
while True:
|
||||
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
|
||||
products += d.get("data", [])
|
||||
if page >= d.get("meta", {}).get("pages", 1):
|
||||
break
|
||||
page += 1
|
||||
|
||||
print(f"{len(products)} products")
|
||||
ok = fail = 0
|
||||
for i, p in enumerate(products):
|
||||
vid = p.get("default_variant_id")
|
||||
if not vid:
|
||||
fail += 1
|
||||
continue
|
||||
r = requests.patch(f"{API}/products/{p['id']}/variants/{vid}", headers=H,
|
||||
json={"count_on_hand": 100, "track_inventory": False}, timeout=60).json()
|
||||
if "error" in r:
|
||||
fail += 1
|
||||
print(f" FAIL {p['slug'][:40]}: {r['error'].get('message')}")
|
||||
else:
|
||||
ok += 1
|
||||
if (i + 1) % 25 == 0:
|
||||
print(f" {i+1}/{len(products)}")
|
||||
time.sleep(0.15)
|
||||
|
||||
print(f"Done. ok={ok} fail={fail}")
|
||||
@@ -0,0 +1,164 @@
|
||||
{"code": "hm-3377-7607064", "product_id": "prod_rsClYtECFI", "slug": "031-h-m-oversized-fit-scuba-zip-through-hoodie", "brand": "H&M", "name": "Oversized Fit Scuba zip-through hoodie", "images": 5, "price_usd": 719.52}
|
||||
{"code": "urban-revivo-3653-8941654", "product_id": "prod_8DR0I7fEbw", "slug": "032-urban-revivo-casual-crew-neck-short-sleeve-tee", "brand": "Urban Revivo", "name": "Casual Crew Neck Short Sleeve Tee", "images": 6, "price_usd": 571.2}
|
||||
{"code": "urban-revivo-4852-8181854", "product_id": "prod_aAtNvLSUAr", "slug": "033-urban-revivo-short-sleeve-shirts", "brand": "Urban Revivo", "name": "Short Sleeve Shirts", "images": 7, "price_usd": 557.28}
|
||||
{"code": "nike-4039-6920264", "product_id": "prod_QiA48cIW7A", "slug": "034-nike-training-t-shirt", "brand": "Nike", "name": "Training T-Shirt", "images": 1, "price_usd": 706.8}
|
||||
{"code": "hm-8811-9083444", "product_id": "prod_QDA48cIW7A", "slug": "035-h-m-regular-fit-track-pants-with-drymove", "brand": "H&M", "name": "Regular Fit Track pants with DryMove\u2122", "images": 8, "price_usd": 719.52}
|
||||
{"code": "trendyol-6397-0057634", "product_id": "prod_8cR0I7fEbw", "slug": "036-trendyol-beige-oversize-wide-cut-long-sleeve-animal-embroidered-sweat", "brand": "Trendyol", "name": "Beige Oversize/Wide Cut Long Sleeve Animal Embroidered Sweatshirt TMNAW24SW00164", "images": 5, "price_usd": 269.04}
|
||||
{"code": "trendyol-9722-5541154", "product_id": "prod_oqlrKdXjaP", "slug": "037-trendyol-horse-embroidered-sweatshirt", "brand": "Trendyol", "name": "Horse Embroidered Sweatshirt", "images": 1, "price_usd": 264.96}
|
||||
{"code": "hm-1602-9539354", "product_id": "prod_48Olu6p4zZ", "slug": "038-h-m-relaxed-fit-resort-shirt", "brand": "H&M", "name": "Relaxed Fit Resort shirt", "images": 6, "price_usd": 479.52}
|
||||
{"code": "trendyol-6524-2362464", "product_id": "prod_qp31JYFduR", "slug": "039-trendyol-anthracite-001-relaxed-mock-neck-plain-sweater-tmnaw26kz0001", "brand": "Trendyol", "name": "Anthracite*001 Relaxed Mock Neck Plain Sweater Tmnaw26Kz00013", "images": 5, "price_usd": 405.6}
|
||||
{"code": "giordano-7623-8198654", "product_id": "prod_JgaEBgznCp", "slug": "040-giordano-men-s-cotton-short-sleeve-slim-tee-01026250", "brand": "Giordano", "name": "Men's Cotton Short Sleeve Slim Tee 01026250", "images": 2, "price_usd": 178.42}
|
||||
{"code": "hm-4373-6317064", "product_id": "prod_tXyJwvFCuK", "slug": "041-h-m-ultra-baggy-jeans", "brand": "H&M", "name": "Ultra Baggy Jeans", "images": 5, "price_usd": 719.52}
|
||||
{"code": "hm-0024-5243544", "product_id": "prod_NJzXvxTk8u", "slug": "042-h-m-relaxed-jeans", "brand": "H&M", "name": "Relaxed Jeans", "images": 4, "price_usd": 623.52}
|
||||
{"code": "ysocool-brand-7905-5999931", "product_id": "prod_XLbb5HTmVa", "slug": "043-ysocool-men-compression-body-shaper-vests-top", "brand": "YSoCool", "name": "Men Compression Body Shaper Vests Top", "images": 3, "price_usd": 223.44}
|
||||
{"code": "nike-8330-6036864", "product_id": "prod_k2KT9MVfb0", "slug": "044-nike-dri-fit-training-t-shirt", "brand": "Nike", "name": "Dri-FIT Training T-Shirt", "images": 1, "price_usd": 542.64}
|
||||
{"code": "hm-8820-5290854", "product_id": "prod_Z5MUPd7wVj", "slug": "045-h-m-loose-fit-t-shirt", "brand": "H&M", "name": "Loose Fit T-shirt", "images": 4, "price_usd": 239.52}
|
||||
{"code": "hm-6806-1878823", "product_id": "prod_STIszOMIvc", "slug": "046-h-m-slim-fit-t-shirt", "brand": "H&M", "name": "Slim Fit T-shirt", "images": 5, "price_usd": 119.52}
|
||||
{"code": "hm-6462-8250054", "product_id": "prod_LYpHOn63Vw", "slug": "047-h-m-relaxed-fit-textured-resort-shirt", "brand": "H&M", "name": "Relaxed Fit Textured resort shirt", "images": 4, "price_usd": 479.52}
|
||||
{"code": "trendyol-3134-8272734", "product_id": "prod_Kj5Q8YFGzd", "slug": "048-trendyol-black-regular-fit-100-viscose-loose-and-loose-summer-shirt-t", "brand": "Trendyol", "name": "Black Regular Fit 100% Viscose Loose and Loose Summer Shirt TMNSS25GO00048", "images": 1, "price_usd": 433.44}
|
||||
{"code": "altinyildiz-8185-0860924", "product_id": "prod_hDBvQ85Ws9", "slug": "049-altinyildiz-basic-slim-fit-t-shirt", "brand": "Altinyildiz", "name": "Basic Slim Fit T-Shirt", "images": 1, "price_usd": 250.37}
|
||||
{"code": "nike-0596-4602954", "product_id": "prod_IOvBlcSU2T", "slug": "050-nike-palm-tree-men-s-tee", "brand": "Nike", "name": "Palm Tree Men's Tee", "images": 1, "price_usd": 446.4}
|
||||
{"code": "lacoste-6939-3474274", "product_id": "prod_K95Q8YFGzd", "slug": "051-lacoste-monogram-jacquard-t-shirt", "brand": "Lacoste", "name": "Monogram Jacquard T-shirt", "images": 4, "price_usd": 2587.2}
|
||||
{"code": "hm-2842-2711854", "product_id": "prod_iIDfaj7G4u", "slug": "052-h-m-relaxed-fit-cotton-chinos", "brand": "H&M", "name": "Relaxed Fit Cotton chinos", "images": 2, "price_usd": 623.52}
|
||||
{"code": "urban-revivo-8524-3567374", "product_id": "prod_6AgxZbenF4", "slug": "053-urban-revivo-classic-solid-color-thick-shirt-in-viscose-blend", "brand": "Urban Revivo", "name": "Classic Solid Color Thick Shirt in Viscose Blend", "images": 8, "price_usd": 907.44}
|
||||
{"code": "hm-4144-4577954", "product_id": "prod_Aks1igzRC6", "slug": "054-h-m-relaxed-fit-oxford-shirt", "brand": "H&M", "name": "Relaxed Fit Oxford shirt", "images": 5, "price_usd": 623.52}
|
||||
{"code": "trendyol-7335-7898364", "product_id": "prod_lmiGAYKIUz", "slug": "055-trendyol-black-regular-normal-cut-hooded-textured-sweatshirt-tmnaw26s", "brand": "Trendyol", "name": "Black Regular/Normal Cut Hooded Textured Sweatshirt Tmnaw26Sw00068", "images": 3, "price_usd": 379.34}
|
||||
{"code": "trendyol-7769-9773614", "product_id": "prod_yar3EWPH85", "slug": "056-trendyol-striped-knit-polo-shirt", "brand": "Trendyol", "name": "Striped Knit Polo Shirt", "images": 4, "price_usd": 410.5}
|
||||
{"code": "hm-4087-3660754", "product_id": "prod_zNcwP3ZbDr", "slug": "057-h-m-regular-fit-hole-knit-sweater-vest", "brand": "H&M", "name": "Regular Fit Hole-knit sweater vest", "images": 6, "price_usd": 671.52}
|
||||
{"code": "hm-9694-6720944", "product_id": "prod_ccuasQi8rC", "slug": "058-h-m-easy-care-relaxed-fit-linen-blend-shorts", "brand": "H&M", "name": "Easy-care Relaxed Fit Linen-blend shorts", "images": 4, "price_usd": 719.52}
|
||||
{"code": "hm-9884-9573454", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "trendyol-1466-7949634", "product_id": "prod_bnnZn4Y5Fq", "slug": "060-trendyol-brown-oversize-wide-cut-buttoned-polo-collar-thick-polar-fle", "brand": "Trendyol", "name": "Brown Oversize/Wide Cut Buttoned Polo Collar Thick Polar Fleece Sweatshirt TMNAW24SW00107", "images": 1, "price_usd": 396.14}
|
||||
{"code": "ben-sherman-1585-5545954", "product_id": "prod_UkKLWZg9DA", "slug": "061-ben-sherman-short-sleeves-jacquard-polo-shirt", "brand": "Ben Sherman", "name": "Short Sleeves Jacquard Polo Shirt", "images": 1, "price_usd": 1500.24}
|
||||
{"code": "hm-9884-9573454", "product_id": "prod_UkpLWZg9DA", "slug": "059-h-m-oversized-fit-t-shirt", "brand": "H&M", "name": "Oversized Fit T-shirt", "images": 6, "price_usd": 335.52}
|
||||
{"code": "trendyol-1466-7949634", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "ben-sherman-1585-5545954", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-3289-5938864", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-3289-5938864", "product_id": "prod_gXZHJdmfrX", "slug": "062-h-m-relaxed-fit-sweatshirt", "brand": "H&M", "name": "Relaxed Fit Sweatshirt", "images": 2, "price_usd": 287.52}
|
||||
{"code": "hm-7368-5259764", "product_id": "prod_Ej9hxLZ9ck", "slug": "063-h-m-regular-fit-easy-iron-shirt", "brand": "H&M", "name": "Regular Fit Easy-iron shirt", "images": 2, "price_usd": 383.52}
|
||||
{"code": "hm-7368-5259764", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-0914-3034864", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-0914-3034864", "product_id": "prod_VZ3XmZF31w", "slug": "064-h-m-polo-shirt", "brand": "H&M", "name": "Polo shirt", "images": 2, "price_usd": 479.52}
|
||||
{"code": "adidas-0957-2958274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "tommy-hilfiger-6431-9934964", "product_id": "prod_OtvJLhNcSb", "slug": "066-tommy-hilfiger-linear-logo-slim-crew-neck-t-shirt-tommy-jeans", "brand": "Tommy Hilfiger", "name": "Linear Logo Slim Crew Neck T-Shirt - Tommy Jeans", "images": 1, "price_usd": 725.04}
|
||||
{"code": "adidas-0957-2958274", "product_id": "prod_uEf2YK1rnl", "slug": "065-adidas-liverpool-fc-26-27-third-jersey", "brand": "ADIDAS", "name": "Liverpool Fc 26/27 Third Jersey", "images": 4, "price_usd": 1579.2}
|
||||
{"code": "tommy-hilfiger-6431-9934964", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "gap-2969-6099274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "gap-2969-6099274", "product_id": "prod_ARbs1igzRC", "slug": "067-gap-logo-t-shirt", "brand": "GAP", "name": "Logo T-Shirt", "images": 1, "price_usd": 479.95}
|
||||
{"code": "gap-0710-4300374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "gap-0710-4300374", "product_id": "prod_p5gklVeMgb", "slug": "068-gap-logo-t-shirt", "brand": "GAP", "name": "Logo T-Shirt", "images": 1, "price_usd": 479.95}
|
||||
{"code": "hm-0924-7104664", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-9961-7990474", "product_id": "prod_9URFqgRua4", "slug": "070-adidas-liverpool-fc-26-27-home-fan-jersey", "brand": "ADIDAS", "name": "Liverpool FC 26/27 Home Fan Jersey", "images": 1, "price_usd": 451.44}
|
||||
{"code": "gap-3366-0689274", "product_id": "prod_xiBoCpeNMW", "slug": "071-gap-pique-polo-shirt", "brand": "GAP", "name": "Pique Polo Shirt", "images": 1, "price_usd": 647.14}
|
||||
{"code": "hm-0924-7104664", "product_id": "prod_nWaqfPa3Cp", "slug": "069-h-m-loose-fit-interlock-t-shirt", "brand": "H&M", "name": "Loose Fit Interlock T-shirt", "images": 6, "price_usd": 239.52}
|
||||
{"code": "adidas-9961-7990474", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "gap-3366-0689274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-6543-9526964", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-6543-9526964", "product_id": "prod_vohmzDoNU0", "slug": "072-adidas-mercedes-amg-petronas-formula-1-team-miami-tee", "brand": "ADIDAS", "name": "Mercedes - AMG Petronas Formula 1 Team Miami Tee", "images": 1, "price_usd": 679.44}
|
||||
{"code": "giordano-8229-4161474", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "giordano-8229-4161474", "product_id": "prod_2h5Y5XM6fr", "slug": "073-giordano-giordano-by-poly-pac-20-24-28-fury-unbreakable-pp-expandable", "brand": "Giordano", "name": "Giordano by Poly Pac 20'' 24\" 28'' Fury UNBREAKABLE PP Expandable Travel Luggage - GA2620 - Black", "images": 6, "price_usd": 3835.2}
|
||||
{"code": "adidas-9235-1679564", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-9235-1679564", "product_id": "prod_HbJVtlDrOm", "slug": "074-adidas-vl-court-base-shoes", "brand": "ADIDAS", "name": "VL Court Base Shoes", "images": 8, "price_usd": 745.92}
|
||||
{"code": "hm-3158-4937164", "product_id": "prod_eH46l8wVqS", "slug": "075-h-m-relaxed-fit-zip-top-rib-knit-jumper", "brand": "H&M", "name": "Relaxed Fit Zip-top rib-knit jumper", "images": 1, "price_usd": 719.52}
|
||||
{"code": "hm-3158-4937164", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-5561-3429464", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "mango-man-6546-3612374", "product_id": "prod_GpMUcnUYI5", "slug": "077-mango-man-fine-knit-cotton-blend-sweater", "brand": "MANGO Man", "name": "Fine Knit Cotton-Blend Sweater", "images": 1, "price_usd": 959.52}
|
||||
{"code": "hm-5561-3429464", "product_id": "prod_se78jaGony", "slug": "076-h-m-loose-fit-scuba-zip-through-hoodie", "brand": "H&M", "name": "Loose Fit Scuba Zip-through hoodie", "images": 2, "price_usd": 719.52}
|
||||
{"code": "mango-man-6546-3612374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-3065-8268274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "adidas-3065-8268274", "product_id": "prod_RgJeNhrwyV", "slug": "078-adidas-trefoil-cushion-crew-socks-3-pairs", "brand": "ADIDAS", "name": "Trefoil Cushion Crew Socks 3 Pairs", "images": 1, "price_usd": 314.64}
|
||||
{"code": "mango-man-5487-0122374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "mango-man-5487-0122374", "product_id": "prod_LKWpHOn63V", "slug": "079-mango-man-fine-knit-sweater-with-zip", "brand": "MANGO Man", "name": "Fine-Knit Sweater With Zip", "images": 1, "price_usd": 1055.52}
|
||||
{"code": "hm-6071-6565464", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "harley-davidson-6140-0807274", "product_id": "prod_CvFSrJgpzQ", "slug": "081-harley-davidson-h-d-tech-vented-short-sleeve-shirt", "brand": "Harley-Davidson", "name": "H-D\u00ae TECH Vented Short Sleeve Shirt", "images": 3, "price_usd": 2409.6}
|
||||
{"code": "hm-6071-6565464", "product_id": "prod_PcANMcnNFW", "slug": "080-h-m-10-pack-mid-trunks-with-lycra", "brand": "H&M", "name": "10-pack mid trunks with Lycra\u00ae", "images": 5, "price_usd": 959.52}
|
||||
{"code": "harley-davidson-6140-0807274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "harley-davidson-6555-0017274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "mango-man-6179-9132374", "product_id": "prod_5sJQZnCRf8", "slug": "083-mango-man-fine-knit-cotton-blend-sweater", "brand": "MANGO Man", "name": "Fine Knit Cotton-Blend Sweater", "images": 1, "price_usd": 959.52}
|
||||
{"code": "harley-davidson-6555-0017274", "product_id": "prod_YBVETxCGlb", "slug": "082-harley-davidson-harley-davidson-vintage-drop-h-ringer-tee", "brand": "Harley-Davidson", "name": "Harley-Davidson\u00ae Vintage Drop H Ringer Tee", "images": 3, "price_usd": 993.6}
|
||||
{"code": "mango-man-6179-9132374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-3272-3490754", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "hm-3272-3490754", "product_id": "prod_FLTGX8NaP3", "slug": "084-h-m-regular-jeans", "brand": "H&M", "name": "Regular Jeans", "images": 3, "price_usd": 479.52}
|
||||
{"code": "levi-s-8540-2089554", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "levi-s-8540-2089554", "product_id": "prod_kQJKT9MVfb", "slug": "085-levi-s-levi-s-men-s-505-regular-jeans-00505-3166", "brand": "Levi's", "name": "Levi's\u00ae Men's 505\u2122 Regular Jeans 00505-3166", "images": 5, "price_usd": 879.36}
|
||||
{"code": "volkswagen-4831-1155093", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "cotton-on-6837-1419164", "product_id": "prod_DzgPsx31Wa", "slug": "087-cotton-on-baggy-jeans", "brand": "Cotton On", "name": "Baggy Jeans", "images": 1, "price_usd": 822.86}
|
||||
{"code": "volkswagen-4831-1155093", "product_id": "prod_3T2WpPTcf5", "slug": "086-volkswagen-slim-long-jeans", "brand": "Volkswagen", "name": "Slim Long Jeans", "images": 4, "price_usd": 763.2}
|
||||
{"code": "cotton-on-6837-1419164", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "levi-s-4894-3133664", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "levi-s-4894-3133664", "product_id": "prod_ml8x3nOJap", "slug": "088-levi-s-levi-s-men-s-505-regular-jeans-00505-3276", "brand": "Levi's", "name": "Levi's\u00ae Men's 505\u2122 Regular Jeans 00505-3276", "images": 3, "price_usd": 879.36}
|
||||
{"code": "volkswagen-4840-0055093", "product_id": "prod_5sSQZnCRf8", "slug": "089-volkswagen-straight-long-jeans", "brand": "Volkswagen", "name": "Straight Long Jeans", "images": 2, "price_usd": 763.2}
|
||||
{"code": "volkswagen-4840-0055093", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "connor-3964-1987644", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "connor-3964-1987644", "product_id": "prod_M3YnyloHRU", "slug": "090-connor-toronto-straight-jean", "brand": "Connor", "name": "Toronto Straight Jean", "images": 3, "price_usd": 690.77}
|
||||
{"code": "levi-s-8200-6615651", "product_id": "prod_p5CklVeMgb", "slug": "091-levi-s-511-slim-fit-jeans-04511-1907", "brand": "Levi's", "name": "511 Slim Fit Jeans 04511-1907", "images": 2, "price_usd": 1071.36}
|
||||
{"code": "levi-s-8200-6615651", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "fidelio-1184-7387944", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "fidelio-1184-7387944", "product_id": "prod_sel8jaGony", "slug": "092-fidelio-601-slim-fit-washed-denim-jeans", "brand": "FIDELIO", "name": "601 Slim Fit Washed Denim Jeans", "images": 3, "price_usd": 523.2}
|
||||
{"code": "levi-s-9015-6656924", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
|
||||
{"code": "drum-3906-4664754", "product_id": "prod_86PR0I7fEb", "slug": "094-drum-drum-casual-skinny-to-slim-fit-jeans-dark-blue", "brand": "DRUM", "name": "DRUM Casual Skinny to Slim Fit Jeans- Dark Blue", "images": 2, "price_usd": 479.52}
|
||||
{"code": "connor-3962-2987644", "product_id": "prod_arstNvLSUA", "slug": "095-connor-arran-straight-jean", "brand": "Connor", "name": "Arran Straight Jean", "images": 5, "price_usd": 690.77}
|
||||
{"code": "trendyol-3864-8297064", "product_id": "prod_Q8DA48cIW7", "slug": "096-trendyol-essential-fit-jeans-denim-pants", "brand": "Trendyol", "name": "Essential Fit Jeans Denim Pants", "images": 1, "price_usd": 520.46}
|
||||
{"code": "electro-denim-lab-7932-5806954", "product_id": "prod_Q8PA48cIW7", "slug": "097-electro-denim-lab-dub-relaxed-fit-jeans", "brand": "Electro Denim Lab", "name": "Dub Relaxed Fit Jeans", "images": 5, "price_usd": 374.4}
|
||||
{"code": "drum-5753-5393434", "product_id": "prod_86uR0I7fEb", "slug": "098-drum-drum-digital-print-denim-straight-cut-jeans-brown", "brand": "DRUM", "name": "DRUM Digital Print Denim Straight Cut Jeans- Brown", "images": 4, "price_usd": 671.52}
|
||||
{"code": "trendyol-4181-4687064", "product_id": "prod_oD3lrKdXja", "slug": "099-trendyol-slim-fit-stretch-fabric-denim-pants-jeans", "brand": "Trendyol", "name": "Slim Fit Stretch Fabric Denim Pants Jeans", "images": 1, "price_usd": 702.24}
|
||||
{"code": "urban-revivo-4820-1281854", "product_id": "prod_4aROlu6p4z", "slug": "100-urban-revivo-denim-shorts", "brand": "Urban Revivo", "name": "Denim Shorts", "images": 6, "price_usd": 431.57}
|
||||
{"code": "hm-2256-5380854", "product_id": "prod_q0k31JYFdu", "slug": "101-h-m-2-pack-regular-fit-shorts", "brand": "H&M", "name": "2-pack Regular Fit Shorts", "images": 2, "price_usd": 479.52}
|
||||
{"code": "hm-5374-3237954", "product_id": "prod_JyHaEBgznC", "slug": "102-h-m-regular-fit-linen-blend-shorts", "brand": "H&M", "name": "Regular Fit Linen-blend shorts", "images": 2, "price_usd": 479.52}
|
||||
{"code": "cotton-on-4000-9099341", "product_id": "prod_tCbyJwvFCu", "slug": "103-cotton-on-easy-shorts", "brand": "Cotton On", "name": "Easy Shorts", "images": 1, "price_usd": 588.24}
|
||||
{"code": "connor-3932-5487644", "product_id": "prod_NuazXvxTk8", "slug": "104-connor-darwin-chino-stretch-short", "brand": "Connor", "name": "Darwin Chino Stretch Short", "images": 3, "price_usd": 561.17}
|
||||
{"code": "fidelio-4233-8849254", "product_id": "prod_XMpbb5HTmV", "slug": "105-fidelio-chino-super-shorts", "brand": "FIDELIO", "name": "Chino Super Shorts", "images": 5, "price_usd": 427.2}
|
||||
{"code": "cotton-on-5986-9744864", "product_id": "prod_kQYKT9MVfb", "slug": "106-cotton-on-easy-active-shorts", "brand": "Cotton On", "name": "Easy Active Shorts", "images": 1, "price_usd": 428.54}
|
||||
{"code": "cotton-on-6085-0724713", "product_id": "prod_ZqQMUPd7wV", "slug": "107-cotton-on-stretch-swim-shorts", "brand": "Cotton On", "name": "Stretch Swim Shorts", "images": 7, "price_usd": 588.24}
|
||||
{"code": "under-armour-0166-9009864", "product_id": "prod_Sm1IszOMIv", "slug": "108-under-armour-challenger-core-men-s-7-5-shorts", "brand": "Under Armour", "name": "Challenger Core Men's 7.5\" Shorts", "images": 1, "price_usd": 360.24}
|
||||
{"code": "cotton-on-1627-2412493", "product_id": "prod_LKEpHOn63V", "slug": "109-cotton-on-everyday-fleece-shorts", "brand": "Cotton On", "name": "Everyday Fleece Shorts", "images": 2, "price_usd": 433.44}
|
||||
{"code": "connor-3936-8387644", "product_id": "prod_KAw5Q8YFGz", "slug": "110-connor-jethro-cargo-short", "brand": "Connor", "name": "Jethro Cargo Short", "images": 1, "price_usd": 561.17}
|
||||
{"code": "nike-9182-7836064", "product_id": "prod_hwPBvQ85Ws", "slug": "111-nike-miler-dri-fit-5-brief-lined-running-shorts", "brand": "Nike", "name": "Miler Dri-FIT 5\" Brief-Lined Running Shorts", "images": 5, "price_usd": 615.6}
|
||||
{"code": "electro-denim-lab-8284-8755973", "product_id": "prod_IxJvBlcSU2", "slug": "112-electro-denim-lab-tailored-shorts", "brand": "Electro Denim Lab", "name": "Tailored Shorts", "images": 4, "price_usd": 345.6}
|
||||
{"code": "electro-denim-lab-5883-6755973", "product_id": "prod_KAF5Q8YFGz", "slug": "113-electro-denim-lab-tailored-shorts", "brand": "Electro Denim Lab", "name": "Tailored Shorts", "images": 3, "price_usd": 345.6}
|
||||
{"code": "under-armour-0103-4898864", "product_id": "prod_iNvDfaj7G4", "slug": "114-under-armour-vanish-woven-2-0-men-s-8-shorts", "brand": "Under Armour", "name": "Vanish Woven 2.0 Men's 8\" Shorts", "images": 2, "price_usd": 668.64}
|
||||
{"code": "under-armour-9965-8909864", "product_id": "prod_6VsgxZbenF", "slug": "115-under-armour-vanish-woven-2-0-men-s-6-shorts", "brand": "Under Armour", "name": "Vanish Woven 2.0 Men's 6\" Shorts", "images": 2, "price_usd": 668.64}
|
||||
{"code": "hm-4916-5208854", "product_id": "prod_ARKs1igzRC", "slug": "116-h-m-oversized-fit-printed-boxy-vest-top", "brand": "H&M", "name": "Oversized Fit Printed boxy vest top", "images": 2, "price_usd": 383.52}
|
||||
{"code": "hm-3300-2208854", "product_id": "prod_lPxiGAYKIU", "slug": "117-h-m-loose-fit-printed-vest-top", "brand": "H&M", "name": "Loose Fit Printed vest top", "images": 2, "price_usd": 143.52}
|
||||
{"code": "hm-0791-0454064", "product_id": "prod_yStr3EWPH8", "slug": "118-h-m-regular-fit-cotton-vest-top", "brand": "H&M", "name": "Regular Fit Cotton vest top", "images": 3, "price_usd": 239.52}
|
||||
{"code": "jack-jones-5952-7458954", "product_id": "prod_z2zcwP3ZbD", "slug": "119-jack-jones-charge-bomber-jacket", "brand": "Jack & Jones", "name": "Charge Bomber Jacket", "images": 1, "price_usd": 673.63}
|
||||
{"code": "jordan-9145-3516754", "product_id": "prod_cE2uasQi8r", "slug": "120-jordan-brooklyn-tricot-track-jacket", "brand": "Jordan", "name": "Brooklyn Tricot Track Jacket", "images": 4, "price_usd": 861.12}
|
||||
{"code": "under-armour-5681-1265724", "product_id": "prod_UKkLWZg9DA", "slug": "121-under-armour-meridian-zip-jacket", "brand": "Under Armour", "name": "Meridian \u00bc Zip Jacket", "images": 1, "price_usd": 861.6}
|
||||
{"code": "desigual-1923-6483534", "product_id": "prod_bqqZn4Y5Fq", "slug": "122-desigual-contrast-collar-jacket", "brand": "Desigual", "name": "Contrast collar jacket", "images": 3, "price_usd": 2373.6}
|
||||
{"code": "mango-man-1932-0456174", "product_id": "prod_UK5LWZg9DA", "slug": "123-mango-man-denim-overshirt-with-pockets", "brand": "MANGO Man", "name": "Denim Overshirt With Pockets", "images": 7, "price_usd": 1727.52}
|
||||
{"code": "nike-3433-7819904", "product_id": "prod_gbMHJdmfrX", "slug": "124-nike-stride-repel-uv-running-jacket", "brand": "Nike", "name": "Stride Repel UV Running Jacket", "images": 1, "price_usd": 1773.84}
|
||||
{"code": "fitleasure-6107-0154414", "product_id": "prod_EwFhxLZ9ck", "slug": "125-fitleasure-men-s-navy-training-track-jacket", "brand": "Fitleasure", "name": "Men's Navy Training Track Jacket", "images": 3, "price_usd": 331.2}
|
||||
{"code": "mango-man-7595-9855154", "product_id": "prod_VMWXmZF31w", "slug": "126-mango-man-wool-overshirt-with-buttons", "brand": "MANGO Man", "name": "Wool Overshirt with Buttons", "images": 1, "price_usd": 1919.52}
|
||||
{"code": "fitleasure-7254-8558544", "product_id": "prod_uh92YK1rnl", "slug": "127-fitleasure-ultraflex-men-s-premium-grey-athleisure-blazer", "brand": "Fitleasure", "name": "UltraFlex Men\u2019s Premium Grey Athleisure Blazer", "images": 6, "price_usd": 448.8}
|
||||
{"code": "tommy-hilfiger-3102-7968464", "product_id": "prod_OymJLhNcSb", "slug": "128-tommy-hilfiger-washed-flag-embroidery-denim-chore-jacket-th-mainline", "brand": "Tommy Hilfiger", "name": "Washed Flag Embroidery Denim Chore Jacket - Th Mainline", "images": 1, "price_usd": 2292.48}
|
||||
{"code": "adidas-6064-9829864", "product_id": "prod_AeZs1igzRC", "slug": "129-adidas-lifestyle-barreda-shoes-men-white-jp5928", "brand": "ADIDAS", "name": "Lifestyle Barreda Shoes Men White JP5928", "images": 1, "price_usd": 803.52}
|
||||
{"code": "adidas-7467-6165014", "product_id": "prod_pQHklVeMgb", "slug": "130-adidas-runfalcon-5-running-shoes", "brand": "ADIDAS", "name": "Runfalcon 5 Running Shoes", "images": 1, "price_usd": 717.12}
|
||||
{"code": "adidas-3835-1386964", "product_id": "prod_n0tqfPa3Cp", "slug": "131-adidas-lifestyle-vl-court-3-0-shoes-men-white-jr8635", "brand": "ADIDAS", "name": "Lifestyle VL Court 3.0 Shoes Men White JR8635", "images": 4, "price_usd": 947.52}
|
||||
{"code": "adidas-3902-2344624", "product_id": "prod_9LeFqgRua4", "slug": "132-adidas-adizero-evo-sl-shoes", "brand": "ADIDAS", "name": "Adizero EVO SL Shoes", "images": 3, "price_usd": 3211.2}
|
||||
{"code": "new-balance-7276-4452864", "product_id": "prod_xDjoCpeNMW", "slug": "133-new-balance-fuelcell-rebel-v5-running-shoes", "brand": "New Balance", "name": "FuelCell Rebel v5 Running Shoes", "images": 1, "price_usd": 2180.64}
|
||||
{"code": "nike-8718-6589064", "product_id": "prod_vlBmzDoNU0", "slug": "134-nike-air-monarch-iv-workout-shoes", "brand": "Nike", "name": "Air Monarch IV Workout Shoes", "images": 2, "price_usd": 825.6}
|
||||
{"code": "under-armour-5894-7391454", "product_id": "prod_2BQY5XM6fr", "slug": "135-under-armour-halo-runner-se-men-s-running-shoes", "brand": "Under Armour", "name": "Halo Runner SE Men's Running Shoes", "images": 5, "price_usd": 1725.12}
|
||||
{"code": "new-balance-0654-0936864", "product_id": "prod_HZaVtlDrOm", "slug": "136-new-balance-603-lifestyle-shoes", "brand": "New Balance", "name": "603 Lifestyle Shoes", "images": 1, "price_usd": 1163.81}
|
||||
{"code": "hm-1277-2755944", "product_id": "prod_eVO6l8wVqS", "slug": "137-h-m-canvas-espadrilles", "brand": "H&M", "name": "Canvas espadrilles", "images": 3, "price_usd": 383.52}
|
||||
{"code": "adidas-7643-4564454", "product_id": "prod_s6d8jaGony", "slug": "138-adidas-handball-spezial-shoes", "brand": "ADIDAS", "name": "Handball Spezial Shoes", "images": 1, "price_usd": 1437.12}
|
||||
{"code": "puma-7967-0821193", "product_id": "prod_GknUcnUYI5", "slug": "139-puma-puma-shuffle", "brand": "PUMA", "name": "Puma Shuffle", "images": 3, "price_usd": 971.04}
|
||||
{"code": "veja-7352-9893832", "product_id": "prod_RHaeNhrwyV", "slug": "140-veja-campo-chromefree-sneakers", "brand": "Veja", "name": "Campo Chromefree Sneakers", "images": 1, "price_usd": 3600.0}
|
||||
{"code": "new-balance-2615-5260964", "product_id": "prod_L50pHOn63V", "slug": "141-new-balance-530-lifestyle-shoes", "brand": "New Balance", "name": "530 Lifestyle Shoes", "images": 1, "price_usd": 1569.46}
|
||||
{"code": "tommy-hilfiger-6391-3924164", "product_id": "prod_PusNMcnNFW", "slug": "142-tommy-hilfiger-the-greenwich-edge-leather-sneakers-tommy-jeans", "brand": "Tommy Hilfiger", "name": "The Greenwich Edge Leather Sneakers - Tommy Jeans", "images": 2, "price_usd": 1615.68}
|
||||
{"code": "veja-6434-3897744", "product_id": "prod_CmGSrJgpzQ", "slug": "143-veja-campo-chromefree-sneakers", "brand": "Veja", "name": "Campo Chromefree Sneakers", "images": 2, "price_usd": 3600.0}
|
||||
{"code": "veja-7346-2897744", "product_id": "prod_YjXETxCGlb", "slug": "144-veja-volley-o-t-leather-sneakers", "brand": "Veja", "name": "Volley O.T. Leather Sneakers", "images": 1, "price_usd": 3480.0}
|
||||
{"code": "new-balance-8119-1352864", "product_id": "prod_58aQZnCRf8", "slug": "145-new-balance-2010-lifestyle-shoes", "brand": "New Balance", "name": "2010 Lifestyle Shoes", "images": 1, "price_usd": 2128.32}
|
||||
{"code": "new-balance-8362-1136064", "product_id": "prod_Fp1GX8NaP3", "slug": "146-new-balance-9060-lifestyle-shoes", "brand": "New Balance", "name": "9060 Lifestyle Shoes", "images": 5, "price_usd": 2106.38}
|
||||
{"code": "hm-3543-9075264", "product_id": "prod_kAaKT9MVfb", "slug": "147-h-m-chelsea-boots", "brand": "H&M", "name": "Chelsea boots", "images": 3, "price_usd": 719.52}
|
||||
{"code": "dr-cardin-3393-7528864", "product_id": "prod_31YWpPTcf5", "slug": "148-dr-cardin-dr-cardin-men-pillow-foam-faux-leather-chelsea-boot-shoe-tbp", "brand": "Dr. Cardin", "name": "Dr Cardin Men Pillow Foam Faux Leather Chelsea Boot Shoe TBP-6855", "images": 7, "price_usd": 420.34}
|
||||
{"code": "hunter-boots-3275-8311344", "product_id": "prod_DcHPsx31Wa", "slug": "149-hunter-boots-men-s-play-short-boots", "brand": "Hunter Boots", "name": "MEN'S PLAY SHORT BOOTS", "images": 6, "price_usd": 2731.2}
|
||||
{"code": "mango-man-1327-3466174", "product_id": "prod_miRx3nOJap", "slug": "150-mango-man-suede-leather-ankle-boots", "brand": "MANGO Man", "name": "Suede Leather Ankle Boots", "images": 1, "price_usd": 2303.52}
|
||||
{"code": "clarks-5776-2553954", "product_id": "prod_58IQZnCRf8", "slug": "151-clarks-mens-wallabee-shoes-blue-green-stripe", "brand": "CLARKS", "name": "Mens Wallabee Shoes - Blue Green Stripe", "images": 2, "price_usd": 3643.2}
|
||||
{"code": "clarks-2605-0299864", "product_id": "prod_MWEnyloHRU", "slug": "152-clarks-mens-paulson-up-boots-black-leather", "brand": "CLARKS", "name": "Mens Paulson Up Boots - Black Leather", "images": 6, "price_usd": 2395.2}
|
||||
{"code": "hm-5557-0718954", "product_id": "prod_pQSklVeMgb", "slug": "153-h-m-regular-fit-merino-wool-jumper", "brand": "H&M", "name": "Regular Fit Merino wool jumper", "images": 5, "price_usd": 959.52}
|
||||
{"code": "hm-3731-3617164", "product_id": "prod_s6i8jaGony", "slug": "154-h-m-regular-fit-rib-knit-cotton-jumper", "brand": "H&M", "name": "Regular Fit Rib-knit cotton jumper", "images": 4, "price_usd": 671.52}
|
||||
{"code": "harley-davidson-0026-6212544", "product_id": "prod_rvRClYtECF", "slug": "155-harley-davidson-h-d-realtree-apg-pullover-hoodie", "brand": "Harley-Davidson", "name": "H-D\u00ae Realtree\u00ae APG Pullover Hoodie", "images": 4, "price_usd": 1881.6}
|
||||
{"code": "gap-6704-2923414", "product_id": "prod_8gNR0I7fEb", "slug": "156-gap-heritage-logo-zip-hoodie", "brand": "GAP", "name": "Heritage Logo Zip Hoodie", "images": 1, "price_usd": 1071.6}
|
||||
{"code": "gap-1231-2925864", "product_id": "prod_aC8tNvLSUA", "slug": "157-gap-arch-sherpa-zip-hoodie", "brand": "GAP", "name": "Arch Sherpa Zip Hoodie", "images": 2, "price_usd": 840.67}
|
||||
{"code": "cotton-on-6809-2319164", "product_id": "prod_QRPA48cIW7", "slug": "158-cotton-on-zip-up-hoodie", "brand": "Cotton On", "name": "Zip Up Hoodie", "images": 1, "price_usd": 573.12}
|
||||
{"code": "cotton-on-6979-5652664", "product_id": "prod_QRNA48cIW7", "slug": "159-cotton-on-box-fit-hoodie", "brand": "Cotton On", "name": "Box Fit Hoodie", "images": 1, "price_usd": 357.6}
|
||||
{"code": "gap-1516-1025214", "product_id": "prod_8g2R0I7fEb", "slug": "160-gap-french-heritage-logo-zip-hoodie", "brand": "GAP", "name": "French Heritage Logo Zip Hoodie", "images": 1, "price_usd": 735.5}
|
||||
@@ -0,0 +1,42 @@
|
||||
require 'json'
|
||||
require 'open-uri'
|
||||
|
||||
store = Spree::Store.default
|
||||
taxonomy = Spree::Taxonomy.find_or_create_by!(name: "Categories", store: store)
|
||||
shipping_category = Spree::ShippingCategory.first_or_create!(name: "Default")
|
||||
|
||||
products_file = "/home/juxtapo/products.jsonl"
|
||||
|
||||
File.readlines(products_file).each_with_index do |line, idx|
|
||||
data = JSON.parse(line)
|
||||
|
||||
taxon = taxonomy.taxons.find_or_create_by!(name: data["brand"]) do |t|
|
||||
t.parent = taxonomy.root
|
||||
end
|
||||
|
||||
product = Spree::Product.create!(
|
||||
name: data["name"],
|
||||
price: data["price"].to_f,
|
||||
description: "#{data["brand"]} - #{data["name"]}",
|
||||
available_on: Time.current,
|
||||
shipping_category: shipping_category,
|
||||
status: "active",
|
||||
stores: [store]
|
||||
)
|
||||
|
||||
product.taxons << taxon
|
||||
|
||||
# attach first image
|
||||
if data["imgs"] && data["imgs"].first
|
||||
begin
|
||||
img_url = data["imgs"].first
|
||||
downloaded = URI.open(img_url)
|
||||
product.images.create!(attachment: { io: downloaded, filename: "#{data["code"]}.jpg" })
|
||||
puts "✅ #{idx+1}/30: #{data["name"]} (#{data["brand"]}) - RM#{data["price"]}"
|
||||
rescue => e
|
||||
puts "⚠️ #{idx+1}/30: #{data["name"]} - image failed: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "Done! Total products: #{Spree::Product.count}"
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pak Jabit — Product Image Pipeline
|
||||
PhotoRoom API: remove background + studio-quality packshot
|
||||
|
||||
Usage:
|
||||
python3 photoroom_pipeline.py --input products/01-gshock-*/image-1.jpg
|
||||
python3 photoroom_pipeline.py --input-dir products/ --all
|
||||
python3 photoroom_pipeline.py --input products/01-gshock-*/image-1.jpg --dry-run
|
||||
|
||||
Env:
|
||||
PHOTOROOM_API_KEY — sandbox_ prefix for testing (watermarked), real key for production
|
||||
"""
|
||||
|
||||
import argparse, glob, json, os, sys, time
|
||||
from pathlib import Path
|
||||
import urllib.request, urllib.error
|
||||
|
||||
API_KEY = os.environ.get("PHOTOROOM_API_KEY")
|
||||
EDIT_URL = "https://image-api.photoroom.com/v2/edit"
|
||||
SEGMENT_URL = "https://sdk.photoroom.com/v1/segment"
|
||||
|
||||
def check_account():
|
||||
req = urllib.request.Request(
|
||||
"https://image-api.photoroom.com/v2/account",
|
||||
headers={"x-api-key": API_KEY},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = json.loads(r.read())
|
||||
print(f"Plan: {data['plan']} | Credits: {data['images']['available']}/{data['images']['subscription']}")
|
||||
return data
|
||||
|
||||
def remove_background(image_path: str, output_path: str):
|
||||
boundary = "----PakJabitBoundary"
|
||||
filename = os.path.basename(image_path)
|
||||
with open(image_path, "rb") as f:
|
||||
image_data = f.read()
|
||||
|
||||
body = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="image_file"; filename="{filename}"\r\n'
|
||||
f"Content-Type: image/jpeg\r\n\r\n"
|
||||
).encode() + image_data + (
|
||||
f"\r\n--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="format"\r\n\r\npng\r\n'
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="size"\r\n\r\nhd\r\n'
|
||||
f"--{boundary}--\r\n"
|
||||
).encode()
|
||||
|
||||
req = urllib.request.Request(SEGMENT_URL, data=body, method="POST", headers={
|
||||
"x-api-key": API_KEY,
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
with open(output_path, "wb") as out:
|
||||
out.write(r.read())
|
||||
return output_path
|
||||
|
||||
def studio_packshot(image_path: str, output_path: str, bg_color: str = "FFFFFF"):
|
||||
boundary = "----PakJabitBoundary"
|
||||
filename = os.path.basename(image_path)
|
||||
with open(image_path, "rb") as f:
|
||||
image_data = f.read()
|
||||
|
||||
fields = [
|
||||
("background.color", bg_color),
|
||||
("padding", "0.1"),
|
||||
("shadow.mode", "ai.soft"),
|
||||
("outputSize", "1200x1600"),
|
||||
("export.format", "jpeg"),
|
||||
("removeBackground", "true"),
|
||||
]
|
||||
|
||||
body = b""
|
||||
for name, value in fields:
|
||||
body += (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'
|
||||
).encode()
|
||||
|
||||
body += (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="imageFile"; filename="{filename}"\r\n'
|
||||
f"Content-Type: image/jpeg\r\n\r\n"
|
||||
).encode() + image_data + f"\r\n--{boundary}--\r\n".encode()
|
||||
|
||||
req = urllib.request.Request(EDIT_URL, data=body, method="POST", headers={
|
||||
"x-api-key": API_KEY,
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
with open(output_path, "wb") as out:
|
||||
out.write(r.read())
|
||||
return output_path
|
||||
|
||||
def process_image(image_path: str, output_dir: str, mode: str = "packshot"):
|
||||
stem = Path(image_path).stem
|
||||
parent = Path(image_path).parent.name
|
||||
|
||||
out_subdir = os.path.join(output_dir, parent)
|
||||
os.makedirs(out_subdir, exist_ok=True)
|
||||
|
||||
if mode == "remove-bg":
|
||||
out_path = os.path.join(out_subdir, f"{stem}-nobg.png")
|
||||
remove_background(image_path, out_path)
|
||||
elif mode == "packshot":
|
||||
out_path = os.path.join(out_subdir, f"{stem}-studio.jpg")
|
||||
studio_packshot(image_path, out_path)
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
print(f" ✅ {os.path.basename(image_path)} → {os.path.basename(out_path)}")
|
||||
return out_path
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="PhotoRoom product image pipeline")
|
||||
parser.add_argument("--input", nargs="+", help="Image file(s) or glob pattern")
|
||||
parser.add_argument("--input-dir", help="Process all images in product subdirs")
|
||||
parser.add_argument("--all", action="store_true", help="Process all image-*.jpg in input-dir")
|
||||
parser.add_argument("--first-only", action="store_true", help="Only process image-1.jpg per product")
|
||||
parser.add_argument("--output-dir", default="products-enhanced", help="Output directory")
|
||||
parser.add_argument("--mode", choices=["remove-bg", "packshot"], default="packshot")
|
||||
parser.add_argument("--dry-run", action="store_true", help="List files without processing")
|
||||
parser.add_argument("--account", action="store_true", help="Check account credits")
|
||||
parser.add_argument("--delay", type=float, default=1.0, help="Delay between API calls (seconds)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not API_KEY:
|
||||
print("❌ Set PHOTOROOM_API_KEY env var first")
|
||||
print(" Sandbox key (free, watermarked): starts with sandbox_")
|
||||
print(" Get it from: https://app.photoroom.com/api-dashboard")
|
||||
sys.exit(1)
|
||||
|
||||
if args.account:
|
||||
check_account()
|
||||
return
|
||||
|
||||
images = []
|
||||
if args.input:
|
||||
for pattern in args.input:
|
||||
images.extend(glob.glob(pattern))
|
||||
elif args.input_dir and args.all:
|
||||
pattern = "image-1.jpg" if args.first_only else "image-*.jpg"
|
||||
images = sorted(glob.glob(os.path.join(args.input_dir, "**", pattern), recursive=True))
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
if not images:
|
||||
print("No images found")
|
||||
return
|
||||
|
||||
print(f"📸 {len(images)} images | mode: {args.mode} | output: {args.output_dir}")
|
||||
|
||||
if args.dry_run:
|
||||
for img in images:
|
||||
print(f" would process: {img}")
|
||||
return
|
||||
|
||||
check_account()
|
||||
print()
|
||||
|
||||
ok, fail = 0, 0
|
||||
for i, img in enumerate(images, 1):
|
||||
print(f"[{i}/{len(images)}] {img}")
|
||||
try:
|
||||
process_image(img, args.output_dir, args.mode)
|
||||
ok += 1
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f" ❌ HTTP {e.code}: {e.read().decode()[:200]}")
|
||||
fail += 1
|
||||
if e.code == 429:
|
||||
print(" ⏳ Rate limited, waiting 30s...")
|
||||
time.sleep(30)
|
||||
except Exception as e:
|
||||
print(f" ❌ {e}")
|
||||
fail += 1
|
||||
|
||||
if i < len(images):
|
||||
time.sleep(args.delay)
|
||||
|
||||
print(f"\n✅ Done: {ok} ok, {fail} failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+218
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pak Jabit — Photoroom return leg.
|
||||
|
||||
The missing half of photoroom_pipeline.py: takes a source image, runs it through
|
||||
Photoroom, and pushes the result back onto the Spree product as a media asset.
|
||||
|
||||
source jpg -> Photoroom /v2/edit -> Spree POST /products/:id/media -> verify
|
||||
|
||||
Verification is not optional here. Spree's media#create has a url path that
|
||||
enqueues a background job and answers `head :accepted` — a 202 with no body and
|
||||
no way to know if the image ever landed. This uses the multipart path, which
|
||||
returns 201 plus the serialized asset, and then re-reads the product's media to
|
||||
confirm the asset id is really there.
|
||||
|
||||
Usage:
|
||||
photoroom_to_spree.py --slug 19-electro-denim-lab-chino-pants --live
|
||||
photoroom_to_spree.py --slug <slug> --position 1 # make it the thumbnail
|
||||
photoroom_to_spree.py --slug <slug> --dry-run
|
||||
|
||||
Keys come from /root/.photoroom/credentials. Sandbox is the default and
|
||||
watermarks every image; --live uses the paid allowance (10/month) and is the
|
||||
only way to get a clean result onto a live storefront.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import pathlib
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
SPREE_API = "https://api.jux-lab.com/api/v3/admin"
|
||||
SPREE_KEY = _spree_admin_key()
|
||||
PHOTOROOM_EDIT = "https://image-api.photoroom.com/v2/edit"
|
||||
CREDS = pathlib.Path("/root/.photoroom/credentials")
|
||||
SOURCE_ROOT = pathlib.Path(
|
||||
"/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products"
|
||||
)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
print(f"FATAL: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_key(live: bool) -> str:
|
||||
if not CREDS.exists():
|
||||
fatal(f"{CREDS} missing — no API key to use")
|
||||
want = "PHOTOROOM_LIVE_KEY" if live else "PHOTOROOM_SANDBOX_KEY"
|
||||
for line in CREDS.read_text().splitlines():
|
||||
if line.startswith(want + "="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
fatal(f"{want} not found in {CREDS}")
|
||||
|
||||
|
||||
def multipart(fields: dict, file_field: str, filename: str, blob: bytes, content_type: str):
|
||||
boundary = uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in fields.items():
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()
|
||||
)
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; '
|
||||
f'filename="{filename}"\r\nContent-Type: {content_type}\r\n\r\n'.encode()
|
||||
+ blob
|
||||
+ b"\r\n"
|
||||
)
|
||||
parts.append(f"--{boundary}--\r\n".encode())
|
||||
return b"".join(parts), f"multipart/form-data; boundary={boundary}"
|
||||
|
||||
|
||||
def spree_get(path):
|
||||
req = urllib.request.Request(
|
||||
f"{SPREE_API}{path}", headers={"X-Spree-Api-Key": SPREE_KEY}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def find_product(slug):
|
||||
data = spree_get(f"/products?filter[slug_eq]={slug}&per_page=50")
|
||||
for p in data.get("data", []):
|
||||
if p.get("slug") == slug or p.get("attributes", {}).get("slug") == slug:
|
||||
return p
|
||||
# The filter key varies by Spree build; fall back to a full scan rather than
|
||||
# reporting "not found" for a product that is actually there.
|
||||
page = 1
|
||||
while True:
|
||||
d = spree_get(f"/products?per_page=100&page={page}")
|
||||
for p in d.get("data", []):
|
||||
if p.get("slug") == slug:
|
||||
return p
|
||||
if page >= d.get("meta", {}).get("pages", 1):
|
||||
return None
|
||||
page += 1
|
||||
|
||||
|
||||
def photoroom(img_path: pathlib.Path, key: str, size: str, bg: str) -> bytes:
|
||||
body, ctype = multipart(
|
||||
{"background.color": bg, "outputSize": size, "padding": "0.05"},
|
||||
"imageFile",
|
||||
img_path.name,
|
||||
img_path.read_bytes(),
|
||||
mimetypes.guess_type(img_path.name)[0] or "image/jpeg",
|
||||
)
|
||||
req = urllib.request.Request(
|
||||
PHOTOROOM_EDIT, data=body, headers={"x-api-key": key, "Content-Type": ctype}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as r:
|
||||
data = r.read()
|
||||
if not data:
|
||||
fatal("Photoroom returned 200 with an empty body")
|
||||
if not r.headers.get("content-type", "").startswith("image/"):
|
||||
fatal(f"Photoroom returned non-image: {r.headers.get('content-type')}")
|
||||
return data
|
||||
except urllib.error.HTTPError as e:
|
||||
fatal(f"Photoroom HTTP {e.code}: {e.read().decode()[:300]}")
|
||||
|
||||
|
||||
def upload_to_spree(product_id: str, blob: bytes, filename: str, position):
|
||||
# position is always sent. Spree defaults a new asset to position 1, which
|
||||
# silently makes it the product thumbnail — a watermarked sandbox image went
|
||||
# live on the storefront that way. Callers pass the append position unless
|
||||
# they explicitly want the front.
|
||||
fields = {"type": "Spree::Image", "position": str(position)}
|
||||
body, ctype = multipart(fields, "attachment", filename, blob, "image/png")
|
||||
req = urllib.request.Request(
|
||||
f"{SPREE_API}/products/{product_id}/media",
|
||||
data=body,
|
||||
headers={"X-Spree-Api-Key": SPREE_KEY, "Content-Type": ctype},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as r:
|
||||
return r.status, json.loads(r.read() or b"{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
fatal(f"Spree HTTP {e.code}: {e.read().decode()[:400]}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--slug", required=True)
|
||||
ap.add_argument("--image", help="source file (default: products/<slug>/image-1.jpg)")
|
||||
ap.add_argument("--live", action="store_true", help="use the paid key (10/month, no watermark)")
|
||||
ap.add_argument("--position", type=int,
|
||||
help="1 makes it the product thumbnail. Default: appended last.")
|
||||
ap.add_argument("--size", default="1000x1000")
|
||||
ap.add_argument("--bg", default="FFFFFF")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
src = pathlib.Path(args.image) if args.image else SOURCE_ROOT / args.slug / "image-1.jpg"
|
||||
if not src.exists():
|
||||
fatal(f"source image not found: {src}")
|
||||
|
||||
product = find_product(args.slug)
|
||||
if not product:
|
||||
fatal(f"no Spree product with slug {args.slug}")
|
||||
pid = product["id"]
|
||||
name = product.get("name", "?")
|
||||
|
||||
before = spree_get(f"/products/{pid}/media").get("data", [])
|
||||
print(f"product : {name} ({pid})")
|
||||
print(f"source : {src} ({src.stat().st_size:,} bytes)")
|
||||
print(f"media : {len(before)} existing")
|
||||
print(f"key : {'LIVE (consumes paid quota)' if args.live else 'sandbox (watermarked)'}")
|
||||
|
||||
if args.dry_run:
|
||||
print("\n--dry-run: nothing sent.")
|
||||
return
|
||||
|
||||
key = load_key(args.live)
|
||||
position = args.position if args.position is not None else len(before) + 1
|
||||
print(f"\nprocessing... (will land at position {position}"
|
||||
f"{' — THUMBNAIL' if position == 1 else ''})")
|
||||
out = photoroom(src, key, args.size, args.bg)
|
||||
print(f" photoroom ok: {len(out):,} bytes png")
|
||||
|
||||
status, resp = upload_to_spree(pid, out, f"{args.slug}-photoroom.png", position)
|
||||
asset_id = resp.get("data", resp).get("id")
|
||||
print(f" spree upload: HTTP {status} asset={asset_id} position={position}")
|
||||
|
||||
# Re-read rather than trusting the response. A 201 is not proof the asset is
|
||||
# attached and serving.
|
||||
after = spree_get(f"/products/{pid}/media").get("data", [])
|
||||
ids = [m.get("id") for m in after]
|
||||
if asset_id and asset_id in ids:
|
||||
url = next((m.get("original_url") or m.get("url") for m in after if m.get("id") == asset_id), None)
|
||||
print(f"\ncheck ok: media {len(before)} -> {len(after)}, asset present")
|
||||
if url:
|
||||
print(f"url: {url}")
|
||||
else:
|
||||
print(f"\nCHECK FAILED: asset {asset_id} not in product media after upload", file=sys.stderr)
|
||||
print(f" media ids: {ids}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
{"brand": "H&M", "code": "hm-3377-7607064", "name": "Oversized Fit Scuba zip-through hoodie", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-3377-7607064-1.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-2.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-3.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-4.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-5.jpg"]}
|
||||
{"brand": "Urban Revivo", "code": "urban-revivo-3653-8941654", "name": "Casual Crew Neck Short Sleeve Tee", "price": "119.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-3653-8941654-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-6.jpg"]}
|
||||
{"brand": "Urban Revivo", "code": "urban-revivo-4852-8181854", "name": "Short Sleeve Shirts", "price": "116.10", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-4852-8181854-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-6.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-7.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-4039-6920264", "name": "Training T-Shirt", "price": "147.25", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-4039-6920264-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-8811-9083444", "name": "Regular Fit Track pants with DryMove™", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-8811-9083444-1.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-2.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-3.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-4.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-5.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-6.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-7.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-8.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-6397-0057634", "name": "Beige Oversize/Wide Cut Long Sleeve Animal Embroidered Sweatshirt TMNAW24SW00164", "price": "56.05", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-6397-0057634-1.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-2.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-3.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-4.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-5.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-9722-5541154", "name": "Horse Embroidered Sweatshirt", "price": "55.20", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-9722-5541154-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-1602-9539354", "name": "Relaxed Fit Resort shirt", "price": "99.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-1602-9539354-1.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-2.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-3.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-4.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-5.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-6.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-6524-2362464", "name": "Anthracite*001 Relaxed Mock Neck Plain Sweater Tmnaw26Kz00013", "price": "84.50", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-6524-2362464-1.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-2.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-3.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-4.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-5.jpg"]}
|
||||
{"brand": "Giordano", "code": "giordano-7623-8198654", "name": "Men's Cotton Short Sleeve Slim Tee 01026250", "price": "37.17", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/giordano-7623-8198654-1.jpg", "https://static-my.zacdn.com/p/giordano-7623-8198654-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-4373-6317064", "name": "Ultra Baggy Jeans", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-4373-6317064-1.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-2.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-3.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-4.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-5.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-0024-5243544", "name": "Relaxed Jeans", "price": "129.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-0024-5243544-1.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-2.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-3.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-4.jpg"]}
|
||||
{"brand": "YSoCool", "code": "ysocool-brand-7905-5999931", "name": "Men Compression Body Shaper Vests Top", "price": "46.55", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-1.jpg", "https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-2.jpg", "https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-3.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-8330-6036864", "name": "Dri-FIT Training T-Shirt", "price": "113.05", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-8330-6036864-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-8820-5290854", "name": "Loose Fit T-shirt", "price": "49.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-8820-5290854-1.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-2.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-3.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-4.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-6806-1878823", "name": "Slim Fit T-shirt", "price": "24.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-6806-1878823-1.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-2.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-3.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-4.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-5.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-6462-8250054", "name": "Relaxed Fit Textured resort shirt", "price": "99.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-6462-8250054-1.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-2.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-3.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-4.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-3134-8272734", "name": "Black Regular Fit 100% Viscose Loose and Loose Summer Shirt TMNSS25GO00048", "price": "90.30", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-3134-8272734-1.jpg"]}
|
||||
{"brand": "Altinyildiz", "code": "altinyildiz-8185-0860924", "name": "Basic Slim Fit T-Shirt", "price": "52.16", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/altinyildiz-8185-0860924-1.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-0596-4602954", "name": "Palm Tree Men's Tee", "price": "93.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-0596-4602954-1.jpg"]}
|
||||
{"brand": "Lacoste", "code": "lacoste-6939-3474274", "name": "Monogram Jacquard T-shirt", "price": "539.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/lacoste-6939-3474274-1.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-2.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-3.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-4.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-2842-2711854", "name": "Relaxed Fit Cotton chinos", "price": "129.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-2842-2711854-1.jpg", "https://static-my.zacdn.com/p/hm-2842-2711854-2.jpg"]}
|
||||
{"brand": "Urban Revivo", "code": "urban-revivo-8524-3567374", "name": "Classic Solid Color Thick Shirt in Viscose Blend", "price": "189.05", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-8524-3567374-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-6.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-7.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-8.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-4144-4577954", "name": "Relaxed Fit Oxford shirt", "price": "129.90", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/hm-4144-4577954-1.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-2.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-3.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-4.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-5.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-7335-7898364", "name": "Black Regular/Normal Cut Hooded Textured Sweatshirt Tmnaw26Sw00068", "price": "79.03", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-7335-7898364-1.jpg", "https://static-my.zacdn.com/p/trendyol-7335-7898364-2.jpg", "https://static-my.zacdn.com/p/trendyol-7335-7898364-3.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-7769-9773614", "name": "Striped Knit Polo Shirt", "price": "85.52", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/trendyol-7769-9773614-1.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-2.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-3.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-4.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-4087-3660754", "name": "Regular Fit Hole-knit sweater vest", "price": "139.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-4087-3660754-1.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-2.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-3.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-4.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-5.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-6.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-9694-6720944", "name": "Easy-care Relaxed Fit Linen-blend shorts", "price": "149.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-9694-6720944-1.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-2.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-3.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-4.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-9884-9573454", "name": "Oversized Fit T-shirt", "price": "69.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-9884-9573454-1.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-2.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-3.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-4.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-5.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-6.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-1466-7949634", "name": "Brown Oversize/Wide Cut Buttoned Polo Collar Thick Polar Fleece Sweatshirt TMNAW24SW00107", "price": "82.53", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/trendyol-1466-7949634-1.jpg"]}
|
||||
{"brand": "Ben Sherman", "code": "ben-sherman-1585-5545954", "name": "Short Sleeves Jacquard Polo Shirt", "price": "312.55", "category": "accessories", "imgs": ["https://static-my.zacdn.com/p/ben-sherman-1585-5545954-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-3289-5938864", "name": "Relaxed Fit Sweatshirt", "price": "59.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-3289-5938864-1.jpg", "https://static-my.zacdn.com/p/hm-3289-5938864-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-7368-5259764", "name": "Regular Fit Easy-iron shirt", "price": "79.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-7368-5259764-1.jpg", "https://static-my.zacdn.com/p/hm-7368-5259764-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-0914-3034864", "name": "Polo shirt", "price": "99.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-0914-3034864-1.jpg", "https://static-my.zacdn.com/p/hm-0914-3034864-2.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-0957-2958274", "name": "Liverpool Fc 26/27 Third Jersey", "price": "329.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-0957-2958274-1.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-2.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-3.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-4.jpg"]}
|
||||
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-6431-9934964", "name": "Linear Logo Slim Crew Neck T-Shirt - Tommy Jeans", "price": "151.05", "category": "new", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-6431-9934964-1.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-2969-6099274", "name": "Logo T-Shirt", "price": "99.99", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-2969-6099274-1.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-0710-4300374", "name": "Logo T-Shirt", "price": "99.99", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-0710-4300374-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-0924-7104664", "name": "Loose Fit Interlock T-shirt", "price": "49.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-0924-7104664-1.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-2.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-3.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-4.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-5.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-6.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-9961-7990474", "name": "Liverpool FC 26/27 Home Fan Jersey", "price": "94.05", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-9961-7990474-1.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-3366-0689274", "name": "Pique Polo Shirt", "price": "134.82", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-3366-0689274-1.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-6543-9526964", "name": "Mercedes - AMG Petronas Formula 1 Team Miami Tee", "price": "141.55", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-6543-9526964-1.jpg"]}
|
||||
{"brand": "Giordano", "code": "giordano-8229-4161474", "name": "Giordano by Poly Pac 20'' 24\" 28'' Fury UNBREAKABLE PP Expandable Travel Luggage - GA2620 - Black", "price": "799.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/giordano-8229-4161474-1.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-2.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-3.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-4.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-5.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-6.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-9235-1679564", "name": "VL Court Base Shoes", "price": "155.40", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-9235-1679564-1.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-2.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-3.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-4.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-5.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-6.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-7.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-8.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-3158-4937164", "name": "Relaxed Fit Zip-top rib-knit jumper", "price": "149.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-3158-4937164-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-5561-3429464", "name": "Loose Fit Scuba Zip-through hoodie", "price": "149.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-5561-3429464-1.jpg", "https://static-my.zacdn.com/p/hm-5561-3429464-2.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-6546-3612374", "name": "Fine Knit Cotton-Blend Sweater", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-6546-3612374-1.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-3065-8268274", "name": "Trefoil Cushion Crew Socks 3 Pairs", "price": "65.55", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-3065-8268274-1.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-5487-0122374", "name": "Fine-Knit Sweater With Zip", "price": "219.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-5487-0122374-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-6071-6565464", "name": "10-pack mid trunks with Lycra®", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-6071-6565464-1.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-2.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-3.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-4.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-5.jpg"]}
|
||||
{"brand": "Harley-Davidson", "code": "harley-davidson-6140-0807274", "name": "H-D® TECH Vented Short Sleeve Shirt", "price": "502.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-6140-0807274-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-6140-0807274-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-6140-0807274-3.jpg"]}
|
||||
{"brand": "Harley-Davidson", "code": "harley-davidson-6555-0017274", "name": "Harley-Davidson® Vintage Drop H Ringer Tee", "price": "207.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-6555-0017274-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-6555-0017274-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-6555-0017274-3.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-6179-9132374", "name": "Fine Knit Cotton-Blend Sweater", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-6179-9132374-1.jpg"]}
|
||||
@@ -0,0 +1,77 @@
|
||||
{"brand": "H&M", "code": "hm-3272-3490754", "name": "Regular Jeans", "price": "99.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/hm-3272-3490754-1.jpg", "https://static-my.zacdn.com/p/hm-3272-3490754-2.jpg", "https://static-my.zacdn.com/p/hm-3272-3490754-3.jpg"]}
|
||||
{"brand": "Levi's", "code": "levi-s-8540-2089554", "name": "Levi's® Men's 505™ Regular Jeans 00505-3166", "price": "183.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-8540-2089554-1.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-2.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-3.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-4.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-5.jpg"]}
|
||||
{"brand": "Volkswagen", "code": "volkswagen-4831-1155093", "name": "Slim Long Jeans", "price": "159.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/volkswagen-4831-1155093-1.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-2.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-3.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-4.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-6837-1419164", "name": "Baggy Jeans", "price": "171.43", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6837-1419164-1.jpg"]}
|
||||
{"brand": "Levi's", "code": "levi-s-4894-3133664", "name": "Levi's® Men's 505™ Regular Jeans 00505-3276", "price": "183.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-4894-3133664-1.jpg", "https://static-my.zacdn.com/p/levi-s-4894-3133664-2.jpg", "https://static-my.zacdn.com/p/levi-s-4894-3133664-3.jpg"]}
|
||||
{"brand": "Volkswagen", "code": "volkswagen-4840-0055093", "name": "Straight Long Jeans", "price": "159.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/volkswagen-4840-0055093-1.jpg", "https://static-my.zacdn.com/p/volkswagen-4840-0055093-2.jpg"]}
|
||||
{"brand": "Connor", "code": "connor-3964-1987644", "name": "Toronto Straight Jean", "price": "143.91", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/connor-3964-1987644-1.jpg", "https://static-my.zacdn.com/p/connor-3964-1987644-2.jpg", "https://static-my.zacdn.com/p/connor-3964-1987644-3.jpg"]}
|
||||
{"brand": "Levi's", "code": "levi-s-8200-6615651", "name": "511 Slim Fit Jeans 04511-1907", "price": "223.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-8200-6615651-1.jpg", "https://static-my.zacdn.com/p/levi-s-8200-6615651-2.jpg"]}
|
||||
{"brand": "FIDELIO", "code": "fidelio-1184-7387944", "name": "601 Slim Fit Washed Denim Jeans", "price": "109.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/fidelio-1184-7387944-1.jpg", "https://static-my.zacdn.com/p/fidelio-1184-7387944-2.jpg", "https://static-my.zacdn.com/p/fidelio-1184-7387944-3.jpg"]}
|
||||
{"brand": "Levi's", "code": "levi-s-9015-6656924", "name": "Men's 511™ Slim Jeans 04511-6225", "price": "239.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-9015-6656924-1.jpg", "https://static-my.zacdn.com/p/levi-s-9015-6656924-2.jpg"]}
|
||||
{"brand": "DRUM", "code": "drum-3906-4664754", "name": "DRUM Casual Skinny to Slim Fit Jeans- Dark Blue", "price": "99.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/drum-3906-4664754-1.jpg", "https://static-my.zacdn.com/p/drum-3906-4664754-2.jpg"]}
|
||||
{"brand": "Connor", "code": "connor-3962-2987644", "name": "Arran Straight Jean", "price": "143.91", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/connor-3962-2987644-1.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-2.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-3.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-4.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-5.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-3864-8297064", "name": "Essential Fit Jeans Denim Pants", "price": "108.43", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/trendyol-3864-8297064-1.jpg"]}
|
||||
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-7932-5806954", "name": "Dub Relaxed Fit Jeans", "price": "78.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-3.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-4.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-5.jpg"]}
|
||||
{"brand": "DRUM", "code": "drum-5753-5393434", "name": "DRUM Digital Print Denim Straight Cut Jeans- Brown", "price": "139.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/drum-5753-5393434-1.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-2.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-3.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-4.jpg"]}
|
||||
{"brand": "Trendyol", "code": "trendyol-4181-4687064", "name": "Slim Fit Stretch Fabric Denim Pants Jeans", "price": "146.30", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/trendyol-4181-4687064-1.jpg"]}
|
||||
{"brand": "Urban Revivo", "code": "urban-revivo-4820-1281854", "name": "Denim Shorts", "price": "89.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-4820-1281854-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-6.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-2256-5380854", "name": "2-pack Regular Fit Shorts", "price": "99.90", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/hm-2256-5380854-1.jpg", "https://static-my.zacdn.com/p/hm-2256-5380854-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-5374-3237954", "name": "Regular Fit Linen-blend shorts", "price": "99.90", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/hm-5374-3237954-1.jpg", "https://static-my.zacdn.com/p/hm-5374-3237954-2.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-4000-9099341", "name": "Easy Shorts", "price": "122.55", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-4000-9099341-1.jpg"]}
|
||||
{"brand": "Connor", "code": "connor-3932-5487644", "name": "Darwin Chino Stretch Short", "price": "116.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/connor-3932-5487644-1.jpg", "https://static-my.zacdn.com/p/connor-3932-5487644-2.jpg", "https://static-my.zacdn.com/p/connor-3932-5487644-3.jpg"]}
|
||||
{"brand": "FIDELIO", "code": "fidelio-4233-8849254", "name": "Chino Super Shorts", "price": "89.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/fidelio-4233-8849254-1.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-2.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-3.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-4.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-5.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-5986-9744864", "name": "Easy Active Shorts", "price": "89.28", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-5986-9744864-1.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-6085-0724713", "name": "Stretch Swim Shorts", "price": "122.55", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6085-0724713-1.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-2.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-3.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-4.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-5.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-6.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-7.jpg"]}
|
||||
{"brand": "Under Armour", "code": "under-armour-0166-9009864", "name": "Challenger Core Men's 7.5\" Shorts", "price": "75.05", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-0166-9009864-1.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-1627-2412493", "name": "Everyday Fleece Shorts", "price": "90.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-1627-2412493-1.jpg", "https://static-my.zacdn.com/p/cotton-on-1627-2412493-2.jpg"]}
|
||||
{"brand": "Connor", "code": "connor-3936-8387644", "name": "Jethro Cargo Short", "price": "116.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/connor-3936-8387644-1.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-9182-7836064", "name": "Miler Dri-FIT 5\" Brief-Lined Running Shorts", "price": "128.25", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/nike-9182-7836064-1.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-2.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-3.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-4.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-5.jpg"]}
|
||||
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-8284-8755973", "name": "Tailored Shorts", "price": "72.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-3.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-4.jpg"]}
|
||||
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-5883-6755973", "name": "Tailored Shorts", "price": "72.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-3.jpg"]}
|
||||
{"brand": "Under Armour", "code": "under-armour-0103-4898864", "name": "Vanish Woven 2.0 Men's 8\" Shorts", "price": "139.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-0103-4898864-1.jpg", "https://static-my.zacdn.com/p/under-armour-0103-4898864-2.jpg"]}
|
||||
{"brand": "Under Armour", "code": "under-armour-9965-8909864", "name": "Vanish Woven 2.0 Men's 6\" Shorts", "price": "139.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-9965-8909864-1.jpg", "https://static-my.zacdn.com/p/under-armour-9965-8909864-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-4916-5208854", "name": "Oversized Fit Printed boxy vest top", "price": "79.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-4916-5208854-1.jpg", "https://static-my.zacdn.com/p/hm-4916-5208854-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-3300-2208854", "name": "Loose Fit Printed vest top", "price": "29.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-3300-2208854-1.jpg", "https://static-my.zacdn.com/p/hm-3300-2208854-2.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-0791-0454064", "name": "Regular Fit Cotton vest top", "price": "49.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-0791-0454064-1.jpg", "https://static-my.zacdn.com/p/hm-0791-0454064-2.jpg", "https://static-my.zacdn.com/p/hm-0791-0454064-3.jpg"]}
|
||||
{"brand": "Jack & Jones", "code": "jack-jones-5952-7458954", "name": "Charge Bomber Jacket", "price": "140.34", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/jack-jones-5952-7458954-1.jpg"]}
|
||||
{"brand": "Jordan", "code": "jordan-9145-3516754", "name": "Brooklyn Tricot Track Jacket", "price": "179.40", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/jordan-9145-3516754-1.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-2.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-3.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-4.jpg"]}
|
||||
{"brand": "Under Armour", "code": "under-armour-5681-1265724", "name": "Meridian ¼ Zip Jacket", "price": "179.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/under-armour-5681-1265724-1.jpg"]}
|
||||
{"brand": "Desigual", "code": "desigual-1923-6483534", "name": "Contrast collar jacket", "price": "494.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/desigual-1923-6483534-1.jpg", "https://static-my.zacdn.com/p/desigual-1923-6483534-2.jpg", "https://static-my.zacdn.com/p/desigual-1923-6483534-3.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-1932-0456174", "name": "Denim Overshirt With Pockets", "price": "359.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/mango-man-1932-0456174-1.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-2.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-3.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-4.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-5.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-6.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-7.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-3433-7819904", "name": "Stride Repel UV Running Jacket", "price": "369.55", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/nike-3433-7819904-1.jpg"]}
|
||||
{"brand": "Fitleasure", "code": "fitleasure-6107-0154414", "name": "Men's Navy Training Track Jacket", "price": "69.00", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/fitleasure-6107-0154414-1.jpg", "https://static-my.zacdn.com/p/fitleasure-6107-0154414-2.jpg", "https://static-my.zacdn.com/p/fitleasure-6107-0154414-3.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-7595-9855154", "name": "Wool Overshirt with Buttons", "price": "399.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/mango-man-7595-9855154-1.jpg"]}
|
||||
{"brand": "Fitleasure", "code": "fitleasure-7254-8558544", "name": "UltraFlex Men’s Premium Grey Athleisure Blazer", "price": "93.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/fitleasure-7254-8558544-1.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-2.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-3.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-4.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-5.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-6.jpg"]}
|
||||
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-3102-7968464", "name": "Washed Flag Embroidery Denim Chore Jacket - Th Mainline", "price": "477.60", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-3102-7968464-1.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-6064-9829864", "name": "Lifestyle Barreda Shoes Men White JP5928", "price": "167.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-6064-9829864-1.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-7467-6165014", "name": "Runfalcon 5 Running Shoes", "price": "149.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-7467-6165014-1.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-3835-1386964", "name": "Lifestyle VL Court 3.0 Shoes Men White JR8635", "price": "197.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-3835-1386964-1.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-2.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-3.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-4.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-3902-2344624", "name": "Adizero EVO SL Shoes", "price": "669.00", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-3902-2344624-1.jpg", "https://static-my.zacdn.com/p/adidas-3902-2344624-2.jpg", "https://static-my.zacdn.com/p/adidas-3902-2344624-3.jpg"]}
|
||||
{"brand": "New Balance", "code": "new-balance-7276-4452864", "name": "FuelCell Rebel v5 Running Shoes", "price": "454.30", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/new-balance-7276-4452864-1.jpg"]}
|
||||
{"brand": "Nike", "code": "nike-8718-6589064", "name": "Air Monarch IV Workout Shoes", "price": "172.00", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/nike-8718-6589064-1.jpg", "https://static-my.zacdn.com/p/nike-8718-6589064-2.jpg"]}
|
||||
{"brand": "Under Armour", "code": "under-armour-5894-7391454", "name": "Halo Runner SE Men's Running Shoes", "price": "359.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/under-armour-5894-7391454-1.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-2.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-3.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-4.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-5.jpg"]}
|
||||
{"brand": "New Balance", "code": "new-balance-0654-0936864", "name": "603 Lifestyle Shoes", "price": "242.46", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/new-balance-0654-0936864-1.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-1277-2755944", "name": "Canvas espadrilles", "price": "79.90", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/hm-1277-2755944-1.jpg", "https://static-my.zacdn.com/p/hm-1277-2755944-2.jpg", "https://static-my.zacdn.com/p/hm-1277-2755944-3.jpg"]}
|
||||
{"brand": "ADIDAS", "code": "adidas-7643-4564454", "name": "Handball Spezial Shoes", "price": "299.40", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/adidas-7643-4564454-1.jpg"]}
|
||||
{"brand": "PUMA", "code": "puma-7967-0821193", "name": "Puma Shuffle", "price": "202.30", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/puma-7967-0821193-1.jpg", "https://static-my.zacdn.com/p/puma-7967-0821193-2.jpg", "https://static-my.zacdn.com/p/puma-7967-0821193-3.jpg"]}
|
||||
{"brand": "Veja", "code": "veja-7352-9893832", "name": "Campo Chromefree Sneakers", "price": "750.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-7352-9893832-1.jpg"]}
|
||||
{"brand": "New Balance", "code": "new-balance-2615-5260964", "name": "530 Lifestyle Shoes", "price": "326.97", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-2615-5260964-1.jpg"]}
|
||||
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-6391-3924164", "name": "The Greenwich Edge Leather Sneakers - Tommy Jeans", "price": "336.60", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-6391-3924164-1.jpg", "https://static-my.zacdn.com/p/tommy-hilfiger-6391-3924164-2.jpg"]}
|
||||
{"brand": "Veja", "code": "veja-6434-3897744", "name": "Campo Chromefree Sneakers", "price": "750.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-6434-3897744-1.jpg", "https://static-my.zacdn.com/p/veja-6434-3897744-2.jpg"]}
|
||||
{"brand": "Veja", "code": "veja-7346-2897744", "name": "Volley O.T. Leather Sneakers", "price": "725.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-7346-2897744-1.jpg"]}
|
||||
{"brand": "New Balance", "code": "new-balance-8119-1352864", "name": "2010 Lifestyle Shoes", "price": "443.40", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-8119-1352864-1.jpg"]}
|
||||
{"brand": "New Balance", "code": "new-balance-8362-1136064", "name": "9060 Lifestyle Shoes", "price": "438.83", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-8362-1136064-1.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-2.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-3.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-4.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-5.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-3543-9075264", "name": "Chelsea boots", "price": "149.90", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/hm-3543-9075264-1.jpg", "https://static-my.zacdn.com/p/hm-3543-9075264-2.jpg", "https://static-my.zacdn.com/p/hm-3543-9075264-3.jpg"]}
|
||||
{"brand": "Dr. Cardin", "code": "dr-cardin-3393-7528864", "name": "Dr Cardin Men Pillow Foam Faux Leather Chelsea Boot Shoe TBP-6855", "price": "87.57", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/dr-cardin-3393-7528864-1.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-2.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-3.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-4.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-5.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-6.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-7.jpg"]}
|
||||
{"brand": "Hunter Boots", "code": "hunter-boots-3275-8311344", "name": "MEN'S PLAY SHORT BOOTS", "price": "569.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/hunter-boots-3275-8311344-1.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-2.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-3.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-4.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-5.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-6.jpg"]}
|
||||
{"brand": "MANGO Man", "code": "mango-man-1327-3466174", "name": "Suede Leather Ankle Boots", "price": "479.90", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/mango-man-1327-3466174-1.jpg"]}
|
||||
{"brand": "CLARKS", "code": "clarks-5776-2553954", "name": "Mens Wallabee Shoes - Blue Green Stripe", "price": "759.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/clarks-5776-2553954-1.jpg", "https://static-my.zacdn.com/p/clarks-5776-2553954-2.jpg"]}
|
||||
{"brand": "CLARKS", "code": "clarks-2605-0299864", "name": "Mens Paulson Up Boots - Black Leather", "price": "499.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/clarks-2605-0299864-1.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-2.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-3.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-4.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-5.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-6.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-5557-0718954", "name": "Regular Fit Merino wool jumper", "price": "199.90", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/hm-5557-0718954-1.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-2.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-3.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-4.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-5.jpg"]}
|
||||
{"brand": "H&M", "code": "hm-3731-3617164", "name": "Regular Fit Rib-knit cotton jumper", "price": "139.90", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/hm-3731-3617164-1.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-2.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-3.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-4.jpg"]}
|
||||
{"brand": "Harley-Davidson", "code": "harley-davidson-0026-6212544", "name": "H-D® Realtree® APG Pullover Hoodie", "price": "392.00", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-0026-6212544-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-3.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-4.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-6704-2923414", "name": "Heritage Logo Zip Hoodie", "price": "223.25", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-6704-2923414-1.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-1231-2925864", "name": "Arch Sherpa Zip Hoodie", "price": "175.14", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-1231-2925864-1.jpg", "https://static-my.zacdn.com/p/gap-1231-2925864-2.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-6809-2319164", "name": "Zip Up Hoodie", "price": "119.40", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6809-2319164-1.jpg"]}
|
||||
{"brand": "Cotton On", "code": "cotton-on-6979-5652664", "name": "Box Fit Hoodie", "price": "74.50", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6979-5652664-1.jpg"]}
|
||||
{"brand": "GAP", "code": "gap-1516-1025214", "name": "French Heritage Logo Zip Hoodie", "price": "153.23", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-1516-1025214-1.jpg"]}
|
||||
@@ -0,0 +1,30 @@
|
||||
{"brand":"G-Shock","code":"g-shock-9407-8856754","name":"DW-5600STT-1 Stranger Things Men Digital Watch Black Resin Band","price":"536.90","imgs":["https://static-my.zacdn.com/p/g-shock-9407-8856754-1.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-2.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-3.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-4.jpg"]}
|
||||
{"brand":"H&M","code":"hm-4512-4118154","name":"Relaxed Fit Printed T-shirt","price":"29.90","imgs":["https://static-my.zacdn.com/p/hm-4512-4118154-1.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-2.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-3.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-4.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-5.jpg"]}
|
||||
{"brand":"H&M","code":"hm-4434-6513734","name":"Regular Jeans","price":"99.90","imgs":["https://static-my.zacdn.com/p/hm-4434-6513734-1.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-2.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-3.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-4.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-5.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-6.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-7.jpg"]}
|
||||
{"brand":"Lacoste","code":"lacoste-2548-0143293","name":"Cotton T-shirt with Breast Pocket","price":"184.50","imgs":["https://static-my.zacdn.com/p/lacoste-2548-0143293-1.jpg","https://static-my.zacdn.com/p/lacoste-2548-0143293-2.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-9447-9378554","name":"Heritage Logo Washed Baseball Cap - Tommy Jeans","price":"116.45","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-9447-9378554-1.jpg"]}
|
||||
{"brand":"Cotton On","code":"cotton-on-4997-4515844","name":"Baggy Worker Pants","price":"114.45","imgs":["https://static-my.zacdn.com/p/cotton-on-4997-4515844-1.jpg"]}
|
||||
{"brand":"GAP","code":"gap-0235-5300064","name":"Arch Logo T-Shirt","price":"60.95","imgs":["https://static-my.zacdn.com/p/gap-0235-5300064-1.jpg"]}
|
||||
{"brand":"Giordano","code":"giordano-4368-3595764","name":"Men's Cotton Short Sleeve Print Tee 13096224","price":"28.03","imgs":["https://static-my.zacdn.com/p/giordano-4368-3595764-1.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-2.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-3.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-4.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-5.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1700-4474664","name":"Square Buckle Leather Belt - TH Mainline","price":"164.34","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1700-4474664-1.jpg"]}
|
||||
{"brand":"Superdry","code":"superdry-9309-2538964","name":"Vintage Script T-Shirt","price":"80.73","imgs":["https://static-my.zacdn.com/p/superdry-9309-2538964-1.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-2.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-3.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-4.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-5.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-6.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-7.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-8.jpg"]}
|
||||
{"brand":"Under Armour","code":"under-armour-4667-9098804","name":"Tech™ Short Sleeve T-Shirt","price":"44.24","imgs":["https://static-my.zacdn.com/p/under-armour-4667-9098804-1.jpg"]}
|
||||
{"brand":"Under Armour","code":"under-armour-8098-5270454","name":"Tech™ Polo Shirt","price":"114.56","imgs":["https://static-my.zacdn.com/p/under-armour-8098-5270454-1.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1774-1674664","name":"Six-Panel Baseball Cap - Th Mainline","price":"143.34","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1774-1674664-1.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-2343-0274664","name":"1985 Regular Polo Shirt - Th Mainline","price":"238.14","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-2343-0274664-1.jpg"]}
|
||||
{"brand":"Cotton On","code":"cotton-on-1718-5169564","name":"Box Fit Text T-Shirt","price":"78.33","imgs":["https://static-my.zacdn.com/p/cotton-on-1718-5169564-1.jpg"]}
|
||||
{"brand":"Giordano","code":"giordano-5996-5838764","name":"Men's Cotton Short Sleeve Print Tee 13096299","price":"28.03","imgs":["https://static-my.zacdn.com/p/giordano-5996-5838764-1.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-2.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-3.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-4.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-5.jpg"]}
|
||||
{"brand":"GAP","code":"gap-7655-7195541","name":"Gap Original Arch Crew","price":"143.36","imgs":["https://static-my.zacdn.com/p/gap-7655-7195541-1.jpg"]}
|
||||
{"brand":"Under Armour","code":"under-armour-3483-5826724","name":"Performance Tech™ Unisex 3-Pack Crew Socks","price":"52.43","imgs":["https://static-my.zacdn.com/p/under-armour-3483-5826724-1.jpg","https://static-my.zacdn.com/p/under-armour-3483-5826724-2.jpg","https://static-my.zacdn.com/p/under-armour-3483-5826724-3.jpg"]}
|
||||
{"brand":"Electro Denim Lab","code":"electro-denim-lab-8086-0906954","name":"Chino Pants","price":"65.00","imgs":["https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-1.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-2.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-3.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-4.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-5.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-2147-5964664","name":"Flag Raglan Sleeves Relaxed Jersey T-Shirt - Th Mainline","price":"179.94","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-2147-5964664-1.jpg"]}
|
||||
{"brand":"Cotton On","code":"cotton-on-6238-0809164","name":"3-Pack Essential Crew Socks","price":"33.05","imgs":["https://static-my.zacdn.com/p/cotton-on-6238-0809164-1.jpg"]}
|
||||
{"brand":"Cotton On","code":"cotton-on-4057-8012493","name":"Everyday Fleece Shorts","price":"60.95","imgs":["https://static-my.zacdn.com/p/cotton-on-4057-8012493-1.jpg"]}
|
||||
{"brand":"Cotton On","code":"cotton-on-6382-0063854","name":"Box Fit Graphic Hoodie","price":"142.03","imgs":["https://static-my.zacdn.com/p/cotton-on-6382-0063854-1.jpg"]}
|
||||
{"brand":"Trendyol","code":"trendyol-3907-9299634","name":"Grimelanj Regular Fit Plaid Knitted Shorts Pajama Set TMNSS25PT00034","price":"79.03","imgs":["https://static-my.zacdn.com/p/trendyol-3907-9299634-1.jpg"]}
|
||||
{"brand":"Camel Active","code":"camel-active-5848-1383954","name":"C BY CAMEL ACTIVE OUTDOOR HIKING TRAVEL BACKPACK - 51250081","price":"269.00","imgs":["https://static-my.zacdn.com/p/camel-active-5848-1383954-1.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-2.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-3.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-4.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-5.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-6.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-7.jpg"]}
|
||||
{"brand":"Lacoste","code":"lacoste-8093-9302554","name":"Cotton T-shirt","price":"258.30","imgs":["https://static-my.zacdn.com/p/lacoste-8093-9302554-1.jpg"]}
|
||||
{"brand":"Under Armour","code":"under-armour-6462-2343264","name":"Under Armour UA Tech™ Vent Men's Short Sleeve","price":"83.44","imgs":["https://static-my.zacdn.com/p/under-armour-6462-2343264-1.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-2.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-3.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-4.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-5.jpg"]}
|
||||
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1355-5874664","name":"Pool Slide Sandals - TH Mainline","price":"191.94","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1355-5874664-1.jpg"]}
|
||||
{"brand":"Calvin Klein","code":"calvin-klein-3281-1558554","name":"Low Rise Trunks 3 Pack - Calvin Klein Underwear","price":"180.45","imgs":["https://static-my.zacdn.com/p/calvin-klein-3281-1558554-1.jpg"]}
|
||||
{"brand":"Swiss Polo","code":"swiss-polo-9699-7969744","name":"Men's Backpack - Black","price":"103.55","imgs":["https://static-my.zacdn.com/p/swiss-polo-9699-7969744-1.jpg","https://static-my.zacdn.com/p/swiss-polo-9699-7969744-2.jpg"]}
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replace product 129's square thumbnail with the reframed 762x1100 version.
|
||||
|
||||
The source was a 2400x2400 square. The storefront grid renders at aspect-3/4
|
||||
with object-cover, so a 1:1 image loses 12.5% off each side — enough to cut the
|
||||
heel and the toe off the shoe. Every other shoe in the catalogue is 762x1100
|
||||
with the subject at ~93% width, so this reframes 129 to match its siblings.
|
||||
|
||||
Uploads the new asset at position 1, verifies it is really there, then removes
|
||||
the square original. --apply is required; the default is a dry run.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
SPREE_API = "https://api.jux-lab.com/api/v3/admin"
|
||||
SPREE_KEY = _spree_admin_key()
|
||||
SLUG = "129-adidas-lifestyle-barreda-shoes-men-white-jp5928"
|
||||
NEW_IMAGE = pathlib.Path(
|
||||
"/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products/"
|
||||
"129-adidas-lifestyle-barreda-shoes-men-white-jp5928/image-1-reframed.jpg"
|
||||
)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
print(f"FATAL: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def multipart(fields, file_field, filename, blob, content_type):
|
||||
boundary = uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in fields.items():
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()
|
||||
)
|
||||
parts.append(
|
||||
f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; '
|
||||
f'filename="{filename}"\r\nContent-Type: {content_type}\r\n\r\n'.encode()
|
||||
+ blob
|
||||
+ b"\r\n"
|
||||
)
|
||||
parts.append(f"--{boundary}--\r\n".encode())
|
||||
return b"".join(parts), f"multipart/form-data; boundary={boundary}"
|
||||
|
||||
|
||||
def api(path, method="GET", data=None, ctype=None):
|
||||
headers = {"X-Spree-Api-Key": SPREE_KEY}
|
||||
if ctype:
|
||||
headers["Content-Type"] = ctype
|
||||
req = urllib.request.Request(
|
||||
f"{SPREE_API}{path}", data=data, headers=headers, method=method
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as r:
|
||||
raw = r.read()
|
||||
return r.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
fatal(f"{method} {path} -> HTTP {e.code}: {e.read().decode()[:400]}")
|
||||
|
||||
|
||||
# This Spree build serves flat objects, not JSON:API — the fields are on the
|
||||
# object itself, not under "attributes" — and it pages on `limit`, ignoring
|
||||
# `per_page` entirely.
|
||||
def find_product(slug):
|
||||
page = 1
|
||||
while True:
|
||||
_, d = api(f"/products?limit=100&page={page}")
|
||||
for p in d.get("data", []):
|
||||
if p.get("slug") == slug:
|
||||
return p
|
||||
if page >= d.get("meta", {}).get("pages", 1):
|
||||
return None
|
||||
page += 1
|
||||
|
||||
|
||||
def media_list(pid):
|
||||
_, d = api(f"/products/{pid}/media?limit=100")
|
||||
return d.get("data", [])
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true", help="actually write to Spree")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not NEW_IMAGE.exists():
|
||||
fatal(f"{NEW_IMAGE} missing")
|
||||
|
||||
p = find_product(SLUG)
|
||||
if not p:
|
||||
fatal(f"product {SLUG} not found")
|
||||
pid = p["id"]
|
||||
print(f"product {pid} {p.get('name')}")
|
||||
|
||||
before = media_list(pid)
|
||||
print(f"media before: {len(before)}")
|
||||
for m in before:
|
||||
print(f" id={m['id']} position={m.get('position')} {str(m.get('original_url',''))[:80]}")
|
||||
|
||||
if not args.apply:
|
||||
print("\nDRY RUN — re-run with --apply to upload and remove the square original.")
|
||||
return
|
||||
|
||||
old_ids = [m["id"] for m in before]
|
||||
|
||||
body, ctype = multipart(
|
||||
{"type": "Spree::Image", "position": "1"},
|
||||
"attachment",
|
||||
NEW_IMAGE.name,
|
||||
NEW_IMAGE.read_bytes(),
|
||||
"image/jpeg",
|
||||
)
|
||||
status, resp = api(f"/products/{pid}/media", "POST", body, ctype)
|
||||
new_id = (resp.get("data") or resp).get("id")
|
||||
print(f"\nupload -> HTTP {status}, new asset id={new_id}")
|
||||
if status == 202 or not new_id:
|
||||
fatal("Spree answered without a serialized asset — cannot confirm the image landed")
|
||||
|
||||
# Re-read rather than trusting the create response: media#create has a path
|
||||
# that enqueues a background job and answers 202 with no body.
|
||||
after = media_list(pid)
|
||||
if new_id not in [m["id"] for m in after]:
|
||||
fatal(f"asset {new_id} is not in the product media after upload")
|
||||
print(f"verified: asset {new_id} present, {len(after)} media on product")
|
||||
|
||||
for oid in old_ids:
|
||||
st, _ = api(f"/products/{pid}/media/{oid}", "DELETE")
|
||||
print(f"removed old asset {oid} -> HTTP {st}")
|
||||
|
||||
final = media_list(pid)
|
||||
print(f"\nmedia after: {len(final)}")
|
||||
for m in final:
|
||||
print(f" id={m['id']} position={m.get('position')} {str(m.get('original_url',''))[:80]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reprice all products from the source MYR catalogue.
|
||||
|
||||
Replaces the prices written by fix_prices.py, which multiplied by 4.8 instead of
|
||||
dividing — putting every product on the store at ~23x its real price.
|
||||
|
||||
Writes BOTH currency rows the store supports:
|
||||
ZAR (store default) = price_myr * MYR_TO_ZAR
|
||||
USD (still in supported_currencies) = price_myr * MYR_TO_USD
|
||||
|
||||
The USD row is corrected in the same pass on purpose: leaving the old 23x rows
|
||||
behind means anyone switching the store currency to USD sees the broken prices
|
||||
again, with nothing to warn them.
|
||||
|
||||
Rates are fetched live and printed. No hardcoded rate, no fallback rate — if the
|
||||
fetch fails this exits rather than pricing a live store off a guess.
|
||||
|
||||
Usage:
|
||||
reprice_zar.py --dry-run # show the changes, write nothing (do this first)
|
||||
reprice_zar.py # apply, then read back and check every row
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
import os as _os
|
||||
|
||||
|
||||
def _spree_admin_key() -> str:
|
||||
"""The Spree admin API key. Required — never defaulted."""
|
||||
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"SPREE_ADMIN_KEY is not set.\n"
|
||||
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
||||
"It is deliberately not stored in this file — see README.md."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
API = "https://api.jux-lab.com/api/v3/admin"
|
||||
H = {"X-Spree-Api-Key": _spree_admin_key(), "Content-Type": "application/json"}
|
||||
PRODUCTS = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products")
|
||||
DRY = "--dry-run" in sys.argv
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
print(f"\nFATAL: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# --- rates -------------------------------------------------------------------
|
||||
def get_rates():
|
||||
"""Live MYR base rates. Loud failure beats a stale constant — see fix_prices.py."""
|
||||
try:
|
||||
d = requests.get("https://open.er-api.com/v6/latest/MYR", timeout=20).json()
|
||||
except Exception as e:
|
||||
fatal(f"rate fetch failed: {e}")
|
||||
r = d.get("rates") or {}
|
||||
zar, usd = r.get("ZAR"), r.get("USD")
|
||||
if not zar or not usd:
|
||||
fatal(f"rate response missing ZAR/USD: {d}")
|
||||
print(f"rates 1 MYR = {zar} ZAR = {usd} USD ({d.get('time_last_update_utc')})")
|
||||
return float(zar), float(usd)
|
||||
|
||||
|
||||
# --- source catalogue --------------------------------------------------------
|
||||
def load_source():
|
||||
prices = {}
|
||||
for folder in sorted(PRODUCTS.iterdir()):
|
||||
info = folder / "info.json"
|
||||
if not info.exists():
|
||||
continue
|
||||
myr = json.loads(info.read_text()).get("price_myr")
|
||||
if myr is None:
|
||||
fatal(f"{folder.name}/info.json has no price_myr")
|
||||
prices[folder.name] = float(myr)
|
||||
if not prices:
|
||||
fatal(f"no source prices found under {PRODUCTS}")
|
||||
print(f"source {len(prices)} products, MYR {min(prices.values()):.2f}-{max(prices.values()):.2f}")
|
||||
return prices
|
||||
|
||||
|
||||
def fetch_products():
|
||||
items, page = [], 1
|
||||
while True:
|
||||
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
|
||||
items += d.get("data", [])
|
||||
if page >= d.get("meta", {}).get("pages", 1):
|
||||
break
|
||||
page += 1
|
||||
return items
|
||||
|
||||
|
||||
def main():
|
||||
zar_rate, usd_rate = get_rates()
|
||||
src = load_source()
|
||||
items = fetch_products()
|
||||
print(f"store {len(items)} products")
|
||||
|
||||
planned, missing = {}, []
|
||||
for p in items:
|
||||
slug = p["slug"]
|
||||
myr = src.get(slug)
|
||||
if myr is None:
|
||||
missing.append(slug)
|
||||
continue
|
||||
planned[p["id"]] = {
|
||||
"slug": slug,
|
||||
"myr": myr,
|
||||
"ZAR": round(myr * zar_rate, 2),
|
||||
"USD": round(myr * usd_rate, 2),
|
||||
}
|
||||
|
||||
if missing:
|
||||
# Not fatal: these keep their current prices. But they must be named, not
|
||||
# silently skipped — a product left at 23x is the whole bug again.
|
||||
print(f"\n{len(missing)} products have NO source price and will be LEFT UNCHANGED:")
|
||||
for s in missing:
|
||||
print(f" ! {s}")
|
||||
|
||||
sample = list(planned.values())[:5]
|
||||
print(f"\n{len(planned)} to reprice. Sample:")
|
||||
for s in sample:
|
||||
print(f" MYR {s['myr']:>7.2f} -> ZAR {s['ZAR']:>8.2f} / USD {s['USD']:>7.2f} {s['slug'][:44]}")
|
||||
zars = [v["ZAR"] for v in planned.values()]
|
||||
print(f" new ZAR range: {min(zars):.2f} - {max(zars):.2f}")
|
||||
|
||||
if DRY:
|
||||
print("\n--dry-run: nothing written.")
|
||||
return
|
||||
|
||||
# --- apply ---------------------------------------------------------------
|
||||
print()
|
||||
ok = fail = 0
|
||||
for n, (pid, s) in enumerate(planned.items(), 1):
|
||||
body = {"prices": [
|
||||
{"amount": s["ZAR"], "currency": "ZAR"},
|
||||
{"amount": s["USD"], "currency": "USD"},
|
||||
]}
|
||||
try:
|
||||
r = requests.patch(f"{API}/products/{pid}", headers=H, json=body, timeout=60).json()
|
||||
except Exception as e:
|
||||
fail += 1
|
||||
print(f" ERROR {s['slug'][:40]}: {e}")
|
||||
continue
|
||||
if "error" in r:
|
||||
fail += 1
|
||||
print(f" FAIL {s['slug'][:40]}: {r['error'].get('message')}")
|
||||
else:
|
||||
ok += 1
|
||||
if n % 40 == 0:
|
||||
print(f" {n}/{len(planned)}")
|
||||
time.sleep(0.12)
|
||||
print(f"patched ok={ok} fail={fail}")
|
||||
|
||||
# --- read back -----------------------------------------------------------
|
||||
# The store returns 200 for a price write that never created a row in the
|
||||
# store currency. Trusting that 200 is what hid 130 products for an hour.
|
||||
print("\nreading back...")
|
||||
time.sleep(2)
|
||||
bad = []
|
||||
for p in fetch_products():
|
||||
want = planned.get(p["id"])
|
||||
if not want:
|
||||
continue
|
||||
a = p.get("attributes", p)
|
||||
got = a.get("price")
|
||||
cur = a.get("currency")
|
||||
if got is None or abs(float(got) - want["ZAR"]) > 0.01 or cur != "ZAR":
|
||||
bad.append((want["slug"], want["ZAR"], got, cur))
|
||||
if bad:
|
||||
print(f"\n{len(bad)} products did NOT take the ZAR price:")
|
||||
for slug, want, got, cur in bad[:15]:
|
||||
print(f" ! {slug[:40]} want ZAR {want} got {got} {cur}")
|
||||
sys.exit(1)
|
||||
print(f"check ok: all {len(planned)} products report the expected ZAR price.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user