feat(products): export hosted detail URL for fetched images; snapshot carries status (SD-0002 §6.5.5, INV-16)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:15:47 -07:00
parent da5177c950
commit 906bc87c96
5 changed files with 36 additions and 10 deletions
+12 -6
View File
@@ -14,6 +14,7 @@ from collections.abc import Iterable, Iterator
from decimal import Decimal
from .diff import CatalogProduct
from .hosted import EXPORT_RENDITION, image_url
from .models import (
IMAGE_COLUMNS,
OPTION_VALUE_COLUMNS,
@@ -51,14 +52,14 @@ def _cell(value: object) -> str:
return str(value)
def catalog_to_csv(products: Iterable[CatalogProduct]) -> Iterator[str]:
def catalog_to_csv(products: Iterable[CatalogProduct], base_url: str = "") -> Iterator[str]:
"""Stream canonical CSV text, header first, one product block at a time."""
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(HEADER)
yield _drain(buf)
for product in products:
for row in _product_rows(product):
for row in _product_rows(product, base_url):
writer.writerow([row.get(col, "") for col in HEADER])
yield _drain(buf)
@@ -70,7 +71,7 @@ def _drain(buf: io.StringIO) -> str:
return text
def _product_rows(product: CatalogProduct) -> list[dict[str, str]]:
def _product_rows(product: CatalogProduct, base_url: str = "") -> list[dict[str, str]]:
"""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."""
@@ -90,13 +91,18 @@ def _product_rows(product: CatalogProduct) -> list[dict[str, str]]:
if i < len(product.variants):
_write_variant(row, product, product.variants[i])
if i < len(product.images):
_write_image(row, product.images[i])
_write_image(row, product.images[i], base_url)
rows.append(row)
return rows
def _write_image(row: dict[str, str], image) -> None:
row["Image Src"] = image.source_url
def _write_image(row: dict[str, str], image, base_url: str) -> None:
# Fetched images export the platform-hosted URL (INV-12/16); everything
# else keeps the original source_url so nothing is lost (§6.5.5).
if image.status == "fetched":
row["Image Src"] = image_url(base_url, image.id, EXPORT_RENDITION)
else:
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