feat(products): serialize multi-variant + interleaved images (§6.5.1 row grammar)
This commit is contained in:
@@ -71,7 +71,9 @@ def _drain(buf: io.StringIO) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _product_rows(product: CatalogProduct) -> list[dict[str, str]]:
|
def _product_rows(product: CatalogProduct) -> list[dict[str, str]]:
|
||||||
"""The product's CSV rows (§6.5.1 grammar). Task 1: first variant only."""
|
"""The product's CSV rows (§6.5.1 grammar): product fields + option names on
|
||||||
|
the first row; one variant per row; images interleaved; image-only rows when
|
||||||
|
a product has more images than variants."""
|
||||||
base = {"Handle": product.handle, "Title": product.title}
|
base = {"Handle": product.handle, "Title": product.title}
|
||||||
for field, col in _PRODUCT_FIELD_TO_COL.items():
|
for field, col in _PRODUCT_FIELD_TO_COL.items():
|
||||||
if field in product.fields:
|
if field in product.fields:
|
||||||
@@ -79,10 +81,25 @@ def _product_rows(product: CatalogProduct) -> list[dict[str, str]]:
|
|||||||
for slot, name in enumerate(product.option_names, start=1):
|
for slot, name in enumerate(product.option_names, start=1):
|
||||||
if name:
|
if name:
|
||||||
base[f"Option{slot} Name"] = name
|
base[f"Option{slot} Name"] = name
|
||||||
variant = product.variants[0]
|
|
||||||
row = dict(base)
|
count = max(len(product.variants), len(product.images))
|
||||||
_write_variant(row, product, variant)
|
rows: list[dict[str, str]] = []
|
||||||
return [row]
|
for i in range(count):
|
||||||
|
# Row 0 carries the product-level fields; later rows carry only Handle.
|
||||||
|
row = dict(base) if i == 0 else {"Handle": product.handle}
|
||||||
|
if i < len(product.variants):
|
||||||
|
_write_variant(row, product, product.variants[i])
|
||||||
|
if i < len(product.images):
|
||||||
|
_write_image(row, product.images[i])
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _write_image(row: dict[str, str], image) -> None:
|
||||||
|
row["Image Src"] = image.source_url
|
||||||
|
row["Image Position"] = str(image.position)
|
||||||
|
if image.alt_text is not None:
|
||||||
|
row["Image Alt Text"] = image.alt_text
|
||||||
|
|
||||||
|
|
||||||
def _write_variant(row: dict[str, str], product: CatalogProduct, variant) -> None:
|
def _write_variant(row: dict[str, str], product: CatalogProduct, variant) -> None:
|
||||||
|
|||||||
@@ -51,3 +51,54 @@ def test_single_no_option_product_one_row():
|
|||||||
assert rows[0]["Variant SKU"] == "WG-MUG"
|
assert rows[0]["Variant SKU"] == "WG-MUG"
|
||||||
# A no-option product emits no option values.
|
# A no-option product emits no option values.
|
||||||
assert rows[0]["Option1 Value"] == ""
|
assert rows[0]["Option1 Value"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def _star_tee() -> CatalogProduct:
|
||||||
|
"""A 3-variant, 2-image product (the sample.csv shape)."""
|
||||||
|
return CatalogProduct(
|
||||||
|
id=2, handle="star-tee", title="Star Tee",
|
||||||
|
option_names=("Size", "Color", None),
|
||||||
|
fields={"title": "Star Tee", "description_html": None, "vendor": "Acme",
|
||||||
|
"product_type": "standalone", "google_product_category": None,
|
||||||
|
"tags": ["apparel", "tees"], "status": "active", "published": True},
|
||||||
|
variants=[
|
||||||
|
CatalogVariant(id=10, options=("S", "Indigo", None), position=1,
|
||||||
|
fields={"sku": "WG-TEE-S", "variant_image": None}),
|
||||||
|
CatalogVariant(id=11, options=("M", "Indigo", None), position=2,
|
||||||
|
fields={"sku": "WG-TEE-M", "variant_image": None}),
|
||||||
|
CatalogVariant(id=12, options=("L", "Indigo", None), position=3,
|
||||||
|
fields={"sku": "WG-TEE-L", "variant_image": None}),
|
||||||
|
],
|
||||||
|
images=[
|
||||||
|
CatalogImage(id=1, source_url="https://x/a.jpg", position=1, alt_text="front"),
|
||||||
|
CatalogImage(id=2, source_url="https://x/b.jpg", position=2, alt_text="back"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multivariant_with_images_interleaves():
|
||||||
|
rows = _rows([_star_tee()])
|
||||||
|
assert len(rows) == 3 # max(3 variants, 2 images)
|
||||||
|
# Product-level fields only on the first row.
|
||||||
|
assert rows[0]["Title"] == "Star Tee" and rows[1]["Title"] == ""
|
||||||
|
assert rows[0]["Tags"] == "apparel, tees" and rows[1]["Tags"] == ""
|
||||||
|
# Option names on row 0; option values on every variant row.
|
||||||
|
assert rows[0]["Option1 Name"] == "Size" and rows[1]["Option1 Name"] == ""
|
||||||
|
assert [r["Option1 Value"] for r in rows] == ["S", "M", "L"]
|
||||||
|
assert [r["Variant SKU"] for r in rows] == ["WG-TEE-S", "WG-TEE-M", "WG-TEE-L"]
|
||||||
|
# Two images on the first two rows; third row has no image.
|
||||||
|
assert [r["Image Src"] for r in rows] == ["https://x/a.jpg", "https://x/b.jpg", ""]
|
||||||
|
assert [r["Image Position"] for r in rows] == ["1", "2", ""]
|
||||||
|
|
||||||
|
|
||||||
|
def test_more_images_than_variants_emits_image_only_rows():
|
||||||
|
p = _product(images=[
|
||||||
|
CatalogImage(id=1, source_url="https://x/a.jpg", position=1, alt_text=None),
|
||||||
|
CatalogImage(id=2, source_url="https://x/b.jpg", position=2, alt_text=None),
|
||||||
|
CatalogImage(id=3, source_url="https://x/c.jpg", position=3, alt_text=None),
|
||||||
|
])
|
||||||
|
rows = _rows([p])
|
||||||
|
assert len(rows) == 3 # 1 variant, 3 images
|
||||||
|
assert rows[0]["Variant SKU"] == "WG-MUG"
|
||||||
|
assert rows[1]["Variant SKU"] == "" and rows[1]["Handle"] == "moon-mug"
|
||||||
|
assert [r["Image Src"] for r in rows] == ["https://x/a.jpg", "https://x/b.jpg", "https://x/c.jpg"]
|
||||||
|
|||||||
Reference in New Issue
Block a user