feat(products): Shopify dialect adapter — header detection + canonical mapping (SD-0002 §6.5.1, SLICE-8)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Shopify product-CSV adapter (INV-17): detect by header signature, map to canonical.
|
||||
|
||||
The mapping is the §6.5.1 contract; the exhaustive table is pinned here and
|
||||
mirrored by tests/test_products_dialect_shopify.py + e2e/fixtures/shopify-export.csv.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .models import KNOWN_COLUMNS
|
||||
|
||||
# Shopify header -> canonical header (renames only; direct same-name columns pass
|
||||
# through via KNOWN_COLUMNS). Variant Grams carries a value transform (see codec).
|
||||
SHOPIFY_RENAME: dict[str, str] = {
|
||||
"Body (HTML)": "Description",
|
||||
"Product Category": "Google Product Category",
|
||||
"Cost per item": "Variant Cost",
|
||||
"Variant Grams": "Variant Weight",
|
||||
}
|
||||
|
||||
# Canonical-named columns Shopify uses differently — must NOT pass through.
|
||||
# Type: Shopify's free-text type vs canonical's structural Type (§6.5.1, decision D).
|
||||
# Variant Weight Unit: superseded by the Variant Grams -> Variant Weight transform.
|
||||
SHOPIFY_DROP: frozenset[str] = frozenset({"Type", "Variant Weight Unit"})
|
||||
|
||||
# Columns whose presence proves the file is a Shopify export (canonical never has them).
|
||||
_SIGNATURE: frozenset[str] = frozenset(
|
||||
{
|
||||
"Body (HTML)",
|
||||
"Variant Grams",
|
||||
"Cost per item",
|
||||
"Variant Compare At Price",
|
||||
"Variant Inventory Policy",
|
||||
"Variant Fulfillment Service",
|
||||
"Variant Requires Shipping",
|
||||
"Variant Taxable",
|
||||
"Gift Card",
|
||||
"SEO Title",
|
||||
"SEO Description",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def is_shopify_header(header: list[str]) -> bool:
|
||||
"""True iff the header carries a Shopify-only signature column. Otherwise the
|
||||
file is treated as canonical — shared columns map identically, so an ambiguous
|
||||
header never misparses (§7.4)."""
|
||||
cols = {h.strip() for h in header}
|
||||
if cols & _SIGNATURE:
|
||||
return True
|
||||
return any(c.startswith("Google Shopping / ") for c in cols)
|
||||
|
||||
|
||||
def map_shopify_header(header: list[str]) -> tuple[list[str | None], list[str]]:
|
||||
"""Return (mapped, not_imported): mapped[i] is the canonical name for header[i],
|
||||
or None when that Shopify column has no canonical home; not_imported lists the
|
||||
original Shopify names (in order) that were dropped — the preview warning."""
|
||||
mapped: list[str | None] = []
|
||||
not_imported: list[str] = []
|
||||
for raw in header:
|
||||
col = raw.strip()
|
||||
if col in SHOPIFY_RENAME:
|
||||
mapped.append(SHOPIFY_RENAME[col])
|
||||
elif col in SHOPIFY_DROP:
|
||||
mapped.append(None)
|
||||
not_imported.append(col)
|
||||
elif col in KNOWN_COLUMNS:
|
||||
mapped.append(col)
|
||||
else:
|
||||
mapped.append(None)
|
||||
if col:
|
||||
not_imported.append(col)
|
||||
return mapped, not_imported
|
||||
Reference in New Issue
Block a user