"""Canonical row model + column registry — the one model every dialect maps to (INV-17).""" from __future__ import annotations from dataclasses import dataclass, field # §6.5.1 canonical columns, by level. Header detection, unknown-column warnings, and # validation all read from this registry. PRODUCT_COLUMNS: dict[str, str] = { # column -> product field name "Title": "title", "Description": "description_html", "Vendor": "vendor", "Type": "product_type", "Google Product Category": "google_product_category", "Tags": "tags", "Status": "status", "Published": "published", "Option1 Name": "option1_name", "Option2 Name": "option2_name", "Option3 Name": "option3_name", } VARIANT_COLUMNS: dict[str, str] = { "Variant SKU": "sku", "Variant Barcode": "barcode", "Variant Price": "price", "Variant Cost": "cost", "Variant Weight": "weight", "Variant Weight Unit": "weight_unit", "Variant Volume": "volume", "Variant Volume Unit": "volume_unit", "Variant Tax ID 1": "tax_id_1", "Variant Tax ID 2": "tax_id_2", "Variant Inventory Tracker": "inventory_tracker", "Variant Inventory Qty": "inventory_qty", "Variant Position": "position", "Variant Image": "variant_image", } OPTION_VALUE_COLUMNS = ("Option1 Value", "Option2 Value", "Option3 Value") IMAGE_COLUMNS = ("Image Src", "Image Position", "Image Alt Text") COMPONENT_COLUMNS = tuple( f"Component {i} {kind}" for i in range(1, 11) for kind in ("SKU", "Quantity") ) KNOWN_COLUMNS = ( {"Handle"} | set(PRODUCT_COLUMNS) | set(VARIANT_COLUMNS) | set(OPTION_VALUE_COLUMNS) | set(IMAGE_COLUMNS) | set(COMPONENT_COLUMNS) ) # Clearing a field (present-but-empty cell, §6.5.1) resets it to its default. CLEAR_DEFAULTS: dict[str, object] = { "status": "active", "published": True, "product_type": "standalone", "tags": [], } MAX_DATA_ROWS = 5_000 # INV-18 MAX_FILE_BYTES = 10 * 1024 * 1024 # INV-18 @dataclass(frozen=True) class Row: """One CSV data row: 1-based file line number + the cells of known columns present in the header (column name -> raw string, possibly empty).""" line_number: int cells: dict[str, str] @dataclass(frozen=True) class ParsedFile: dialect: str header: list[str] unknown_columns: list[str] rows: list[Row] @dataclass(frozen=True) class RowError: line_number: int column: str | None message: str def as_json(self) -> dict: return {"line": self.line_number, "column": self.column, "message": self.message} @dataclass class CanonicalVariant: line_number: int options: tuple[str | None, str | None, str | None] # field name -> normalized value; present only for columns in the file. # value None == clear (reset to default/NULL). fields: dict[str, object] = field(default_factory=dict) @dataclass class CanonicalImage: line_number: int source_url: str position: int alt_text: str | None @dataclass class CanonicalProduct: first_line: int handle: str title: str # "" when missing (the block then carries an error) option_names: tuple[str | None, str | None, str | None] = (None, None, None) fields: dict[str, object] = field(default_factory=dict) # product-level, same semantics variants: list[CanonicalVariant] = field(default_factory=list) images: list[CanonicalImage] = field(default_factory=list) errors: list[RowError] = field(default_factory=list) @property def valid(self) -> bool: return not self.errors