shelfshift 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. shelfshift/__init__.py +48 -0
  2. shelfshift/cli/__init__.py +1 -0
  3. shelfshift/cli/main.py +190 -0
  4. shelfshift/config.py +72 -0
  5. shelfshift/core/__init__.py +79 -0
  6. shelfshift/core/api.py +227 -0
  7. shelfshift/core/canonical/__init__.py +60 -0
  8. shelfshift/core/canonical/entities.py +643 -0
  9. shelfshift/core/canonical/helpers.py +256 -0
  10. shelfshift/core/canonical/schemas.py +21 -0
  11. shelfshift/core/canonical/serialization.py +17 -0
  12. shelfshift/core/config.py +32 -0
  13. shelfshift/core/detect/__init__.py +4 -0
  14. shelfshift/core/detect/csv.py +5 -0
  15. shelfshift/core/detect/url.py +134 -0
  16. shelfshift/core/exporters/__init__.py +42 -0
  17. shelfshift/core/exporters/api.py +51 -0
  18. shelfshift/core/exporters/platforms/__init__.py +2 -0
  19. shelfshift/core/exporters/platforms/bigcommerce.py +486 -0
  20. shelfshift/core/exporters/platforms/shopify.py +216 -0
  21. shelfshift/core/exporters/platforms/squarespace.py +166 -0
  22. shelfshift/core/exporters/platforms/wix.py +310 -0
  23. shelfshift/core/exporters/platforms/woocommerce.py +332 -0
  24. shelfshift/core/exporters/shared/__init__.py +2 -0
  25. shelfshift/core/exporters/shared/batch.py +164 -0
  26. shelfshift/core/exporters/shared/utils.py +302 -0
  27. shelfshift/core/exporters/shared/weight_units.py +36 -0
  28. shelfshift/core/importers/__init__.py +10 -0
  29. shelfshift/core/importers/csv/__init__.py +58 -0
  30. shelfshift/core/importers/csv/batch.py +310 -0
  31. shelfshift/core/importers/csv/bigcommerce.py +226 -0
  32. shelfshift/core/importers/csv/common.py +348 -0
  33. shelfshift/core/importers/csv/detection.py +37 -0
  34. shelfshift/core/importers/csv/shopify.py +122 -0
  35. shelfshift/core/importers/csv/squarespace.py +113 -0
  36. shelfshift/core/importers/csv/wix.py +125 -0
  37. shelfshift/core/importers/csv/woocommerce.py +139 -0
  38. shelfshift/core/importers/url/__init__.py +105 -0
  39. shelfshift/core/importers/url/api.py +93 -0
  40. shelfshift/core/importers/url/common.py +303 -0
  41. shelfshift/core/importers/url/platforms/__init__.py +20 -0
  42. shelfshift/core/importers/url/platforms/aliexpress.py +404 -0
  43. shelfshift/core/importers/url/platforms/amazon.py +308 -0
  44. shelfshift/core/importers/url/platforms/shopify.py +391 -0
  45. shelfshift/core/importers/url/platforms/squarespace.py +829 -0
  46. shelfshift/core/importers/url/platforms/woocommerce.py +694 -0
  47. shelfshift/core/registry.py +98 -0
  48. shelfshift/core/validate/__init__.py +4 -0
  49. shelfshift/core/validate/report.py +21 -0
  50. shelfshift/core/validate/rules.py +33 -0
  51. shelfshift/server/__init__.py +3 -0
  52. shelfshift/server/config.py +15 -0
  53. shelfshift/server/helpers/__init__.py +40 -0
  54. shelfshift/server/helpers/exporting.py +169 -0
  55. shelfshift/server/helpers/importing.py +150 -0
  56. shelfshift/server/helpers/payload.py +46 -0
  57. shelfshift/server/helpers/rendering.py +94 -0
  58. shelfshift/server/logging/__init__.py +3 -0
  59. shelfshift/server/logging/product_payloads.py +183 -0
  60. shelfshift/server/main.py +62 -0
  61. shelfshift/server/routers/__init__.py +3 -0
  62. shelfshift/server/routers/api.py +200 -0
  63. shelfshift/server/routers/web_csv.py +134 -0
  64. shelfshift/server/routers/web_url.py +314 -0
  65. shelfshift/server/schemas.py +81 -0
  66. shelfshift/server/web/static/app.js +904 -0
  67. shelfshift/server/web/static/favicon.ico +0 -0
  68. shelfshift/server/web/static/shelfshift_logo.png +0 -0
  69. shelfshift/server/web/static/styles.css +1077 -0
  70. shelfshift/server/web/templates/_export_form.html +59 -0
  71. shelfshift/server/web/templates/_product_editor.html +163 -0
  72. shelfshift/server/web/templates/_product_editor_batch.html +179 -0
  73. shelfshift/server/web/templates/base.html +78 -0
  74. shelfshift/server/web/templates/csv.html +85 -0
  75. shelfshift/server/web/templates/index.html +65 -0
  76. shelfshift/server/web/templates/url.html +78 -0
  77. shelfshift-1.0.0.dist-info/METADATA +381 -0
  78. shelfshift-1.0.0.dist-info/RECORD +82 -0
  79. shelfshift-1.0.0.dist-info/WHEEL +5 -0
  80. shelfshift-1.0.0.dist-info/entry_points.txt +3 -0
  81. shelfshift-1.0.0.dist-info/licenses/LICENSE +21 -0
  82. shelfshift-1.0.0.dist-info/top_level.txt +1 -0
shelfshift/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Public package entrypoint for the Shelfshift engine.
2
+
3
+ This package provides a stable import surface for core e-commerce catalog
4
+ import/export logic, plus optional frontend adapters (CLI and FastAPI server).
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+ from typing import Any
9
+
10
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
11
+ "Product": ("shelfshift.core", "Product"),
12
+ "app": ("shelfshift.server.main", "app"),
13
+ "create_app": ("shelfshift.server.main", "create_app"),
14
+ "detect_csv_platform": ("shelfshift.core", "detect_csv_platform"),
15
+ "detect_product_url": ("shelfshift.core", "detect_product_url"),
16
+ "export_csv_for_target": ("shelfshift.core", "export_csv_for_target"),
17
+ "import_product_from_csv": ("shelfshift.core", "import_product_from_csv"),
18
+ "import_product_from_url": ("shelfshift.core", "import_product_from_url"),
19
+ }
20
+
21
+ try:
22
+ __version__ = version("shelfshift")
23
+ except PackageNotFoundError:
24
+ __version__ = "0.0.0"
25
+
26
+ __all__ = [
27
+ "Product",
28
+ "__version__",
29
+ "app",
30
+ "create_app",
31
+ "detect_csv_platform",
32
+ "detect_product_url",
33
+ "export_csv_for_target",
34
+ "import_product_from_csv",
35
+ "import_product_from_url",
36
+ ]
37
+
38
+
39
+ def __getattr__(name: str) -> Any:
40
+ target = _LAZY_EXPORTS.get(name)
41
+ if target is None:
42
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
43
+
44
+ module_name, attribute_name = target
45
+ module = __import__(module_name, fromlist=[attribute_name])
46
+ value = getattr(module, attribute_name)
47
+ globals()[name] = value
48
+ return value
@@ -0,0 +1 @@
1
+ """CLI frontend for the Shelfshift core engine."""
shelfshift/cli/main.py ADDED
@@ -0,0 +1,190 @@
1
+ """Command-line frontend for the Shelfshift core engine."""
2
+
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from dotenv import load_dotenv
10
+
11
+ from shelfshift.core.config import resolve_rapidapi_key
12
+ from shelfshift.core import (
13
+ convert_csv,
14
+ detect_csv,
15
+ detect_url,
16
+ export_csv,
17
+ import_csv,
18
+ import_url,
19
+ parse_product_payload,
20
+ validate,
21
+ )
22
+
23
+ load_dotenv(Path(__file__).resolve().parents[2] / ".env")
24
+
25
+
26
+ def _json_dump(data: Any) -> None:
27
+ print(json.dumps(data, ensure_ascii=False, indent=2))
28
+
29
+
30
+ def _cmd_detect_url(args: argparse.Namespace) -> int:
31
+ _json_dump(detect_url(args.input).__dict__)
32
+ return 0
33
+
34
+
35
+ def _cmd_detect_csv(args: argparse.Namespace) -> int:
36
+ _json_dump(detect_csv(args.input).__dict__)
37
+ return 0
38
+
39
+
40
+ def _cmd_import_url(args: argparse.Namespace) -> int:
41
+ resolved_rapidapi_key = resolve_rapidapi_key(args.rapidapi_key)
42
+ result = import_url(
43
+ args.url,
44
+ strict=args.strict,
45
+ rapidapi_key=resolved_rapidapi_key,
46
+ )
47
+ _json_dump(
48
+ {
49
+ "products": [p.to_dict(include_raw=args.include_raw) for p in result.products],
50
+ "errors": result.errors,
51
+ }
52
+ )
53
+ return 0
54
+
55
+
56
+ def _cmd_import_csv(args: argparse.Namespace) -> int:
57
+ result = import_csv(
58
+ args.input,
59
+ platform=args.source_platform,
60
+ strict=args.strict,
61
+ source_weight_unit=args.source_weight_unit,
62
+ )
63
+ _json_dump(
64
+ {
65
+ "products": [p.to_dict(include_raw=args.include_raw) for p in result.products],
66
+ "errors": result.errors,
67
+ }
68
+ )
69
+ return 0
70
+
71
+
72
+ def _cmd_convert(args: argparse.Namespace) -> int:
73
+ csv_bytes, report = convert_csv(
74
+ args.input,
75
+ target=args.to,
76
+ source=args.source,
77
+ strict=args.strict,
78
+ source_weight_unit=args.source_weight_unit,
79
+ export_options={"weight_unit": args.weight_unit},
80
+ )
81
+ out_path = Path(args.out)
82
+ out_path.write_bytes(csv_bytes)
83
+ if args.report:
84
+ Path(args.report).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
85
+ _json_dump({"output": str(out_path), "report": report})
86
+ return 0
87
+
88
+
89
+ def _cmd_validate(args: argparse.Namespace) -> int:
90
+ result = import_csv(
91
+ args.input,
92
+ platform=args.platform,
93
+ strict=args.strict,
94
+ source_weight_unit=args.source_weight_unit,
95
+ )
96
+ reports = validate(result.products)
97
+ payload = [
98
+ {
99
+ "valid": report.valid,
100
+ "issues": [issue.__dict__ for issue in report.issues],
101
+ }
102
+ for report in reports
103
+ ]
104
+ if args.report:
105
+ Path(args.report).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
106
+ _json_dump(payload)
107
+ return 0
108
+
109
+
110
+ def build_parser() -> argparse.ArgumentParser:
111
+ parser = argparse.ArgumentParser(prog="shelfshift", description="Shelfshift core engine CLI")
112
+ subparsers = parser.add_subparsers(dest="command", required=True)
113
+
114
+ detect = subparsers.add_parser("detect", help="Detect input kind/platform from URL or CSV path")
115
+ detect.add_argument("input", help="URL or CSV file path")
116
+ detect.set_defaults(func=lambda args: _cmd_detect_csv(args) if Path(args.input).exists() else _cmd_detect_url(args))
117
+
118
+ import_url_cmd = subparsers.add_parser("import-url", help="Import canonical products from one or more URLs")
119
+ import_url_cmd.add_argument("url", nargs="+", help="One or more product URLs")
120
+ import_url_cmd.add_argument("--rapidapi-key", default=None)
121
+ import_url_cmd.add_argument("--include-raw", action="store_true")
122
+ import_url_cmd.add_argument("--strict", action="store_true")
123
+ import_url_cmd.set_defaults(func=lambda args: _cmd_import_url(argparse.Namespace(**{**vars(args), "url": args.url if len(args.url) > 1 else args.url[0]})))
124
+
125
+ convert = subparsers.add_parser("convert", help="Convert source CSV to target platform CSV")
126
+ convert.add_argument("input", help="Source CSV file path")
127
+ convert.add_argument("--to", required=True, choices=["shopify", "bigcommerce", "wix", "squarespace", "woocommerce"])
128
+ convert.add_argument("--source", default=None)
129
+ convert.add_argument("--source-weight-unit", default="")
130
+ convert.add_argument("--weight-unit", default="")
131
+ convert.add_argument("--strict", action="store_true")
132
+ convert.add_argument("--out", required=True)
133
+ convert.add_argument("--report", default="")
134
+ convert.set_defaults(func=_cmd_convert)
135
+
136
+ validate_cmd = subparsers.add_parser("validate", help="Validate canonicalized products imported from source CSV")
137
+ validate_cmd.add_argument("input", help="Source CSV file path")
138
+ validate_cmd.add_argument("--platform", default=None)
139
+ validate_cmd.add_argument("--source-weight-unit", default="")
140
+ validate_cmd.add_argument("--strict", action="store_true")
141
+ validate_cmd.add_argument("--report", default="")
142
+ validate_cmd.set_defaults(func=_cmd_validate)
143
+
144
+ import_csv_cmd = subparsers.add_parser("import-csv", help="Import canonical products from source CSV")
145
+ import_csv_cmd.add_argument("input", help="Source CSV file path")
146
+ import_csv_cmd.add_argument("--source-platform", default=None)
147
+ import_csv_cmd.add_argument("--source-weight-unit", default="")
148
+ import_csv_cmd.add_argument("--include-raw", action="store_true")
149
+ import_csv_cmd.add_argument("--strict", action="store_true")
150
+ import_csv_cmd.set_defaults(func=_cmd_import_csv)
151
+
152
+ export_csv_cmd = subparsers.add_parser("export-csv", help="Export canonical JSON payload to target CSV")
153
+ export_csv_cmd.add_argument("input", help="Canonical product JSON path")
154
+ export_csv_cmd.add_argument("--to", required=True, choices=["shopify", "bigcommerce", "wix", "squarespace", "woocommerce"])
155
+ export_csv_cmd.add_argument("--weight-unit", default="")
156
+ export_csv_cmd.add_argument("--out", required=True)
157
+ export_csv_cmd.add_argument("--report", default="")
158
+ export_csv_cmd.set_defaults(func=_cmd_export_csv)
159
+
160
+ return parser
161
+
162
+
163
+ def _cmd_export_csv(args: argparse.Namespace) -> int:
164
+ payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
165
+ products = parse_product_payload(payload)
166
+
167
+ exported = export_csv(
168
+ products,
169
+ target=args.to,
170
+ options={"weight_unit": args.weight_unit},
171
+ )
172
+ Path(args.out).write_bytes(exported.csv_bytes)
173
+ report = {"filename": exported.filename, "target_platform": args.to}
174
+ if args.report:
175
+ Path(args.report).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
176
+ _json_dump({"output": args.out, "report": report})
177
+ return 0
178
+
179
+
180
+ def main(argv: list[str] | None = None) -> int:
181
+ parser = build_parser()
182
+ args = parser.parse_args(argv)
183
+ try:
184
+ return int(args.func(args) or 0)
185
+ except Exception as exc:
186
+ parser.exit(status=2, message=f"error: {exc}\n")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())
shelfshift/config.py ADDED
@@ -0,0 +1,72 @@
1
+ """Shared runtime settings for server/web adapters.
2
+
3
+ This module owns environment-backed application settings. It is intentionally
4
+ separate from ``shelfshift.core.config`` because core config stays minimal and
5
+ framework-agnostic.
6
+ """
7
+
8
+ import os
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Settings:
14
+ app_name: str
15
+ app_tagline: str
16
+ brand_primary: str
17
+ brand_secondary: str
18
+ brand_ink: str
19
+ debug: bool
20
+ log_verbosity: str
21
+ rapidapi_key: str | None
22
+ cors_allow_origins: tuple[str, ...]
23
+
24
+
25
+ def _env_bool(name: str, default: bool = False) -> bool:
26
+ val = os.getenv(name)
27
+ if val is None:
28
+ return default
29
+ return val.strip().lower() in {"1", "true", "yes", "on"}
30
+
31
+
32
+ def _env_choice(name: str, default: str, *, allowed: set[str]) -> str:
33
+ val = os.getenv(name)
34
+ if val is None:
35
+ return default
36
+ normalized = val.strip().lower()
37
+ if normalized in allowed:
38
+ return normalized
39
+ return default
40
+
41
+
42
+ def settings_from_env() -> Settings:
43
+ origins = tuple(
44
+ origin.strip()
45
+ for origin in os.getenv("CORS_ALLOW_ORIGINS", "*").split(",")
46
+ if origin.strip()
47
+ )
48
+ return Settings(
49
+ app_name=os.getenv("APP_NAME", "ShelfShift"),
50
+ app_tagline=os.getenv(
51
+ "APP_TAGLINE",
52
+ "Developer toolkit for ecommerce catalog translation.",
53
+ ),
54
+ brand_primary=os.getenv("BRAND_PRIMARY", "#18d9b6"),
55
+ brand_secondary=os.getenv("BRAND_SECONDARY", "#27c6f5"),
56
+ brand_ink=os.getenv("BRAND_INK", "#020b1a"),
57
+ debug=_env_bool("DEBUG", default=False),
58
+ log_verbosity=_env_choice(
59
+ "LOG_VERBOSITY",
60
+ default="medium",
61
+ allowed={"low", "medium", "high", "extrahigh"},
62
+ ),
63
+ rapidapi_key=os.getenv("RAPIDAPI_KEY"),
64
+ cors_allow_origins=origins or ("*",),
65
+ )
66
+
67
+
68
+ def get_settings() -> Settings:
69
+ return settings_from_env()
70
+
71
+
72
+ __all__ = ["Settings", "get_settings", "settings_from_env"]
@@ -0,0 +1,79 @@
1
+ """Core engine API.
2
+
3
+ The core layer is framework-agnostic and safe to import from scripts, tests,
4
+ CLI commands, and web frontends.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
10
+ "CoreConfig": ("shelfshift.core.config", "CoreConfig"),
11
+ "DetectResult": ("shelfshift.core.api", "DetectResult"),
12
+ "ExportResult": ("shelfshift.core.api", "ExportResult"),
13
+ "ImportResult": ("shelfshift.core.api", "ImportResult"),
14
+ "Product": ("shelfshift.core.canonical.entities", "Product"),
15
+ "config_from_env": ("shelfshift.core.config", "config_from_env"),
16
+ "convert_csv": ("shelfshift.core.api", "convert_csv"),
17
+ "detect_csv": ("shelfshift.core.api", "detect_csv"),
18
+ "detect_csv_platform": ("shelfshift.core.detect.csv", "detect_csv_platform"),
19
+ "detect_product_url": ("shelfshift.core.detect.url", "detect_product_url"),
20
+ "detect_url": ("shelfshift.core.api", "detect_url"),
21
+ "export_csv": ("shelfshift.core.api", "export_csv"),
22
+ "export_csv_for_target": ("shelfshift.core.exporters", "export_csv_for_target"),
23
+ "get_exporter": ("shelfshift.core.registry", "get_exporter"),
24
+ "get_importer": ("shelfshift.core.registry", "get_importer"),
25
+ "import_csv": ("shelfshift.core.api", "import_csv"),
26
+ "import_product_from_csv": ("shelfshift.core.importers.csv", "import_product_from_csv"),
27
+ "import_product_from_url": ("shelfshift.core.importers.url", "import_product_from_url"),
28
+ "import_products_from_csv": ("shelfshift.core.importers.csv", "import_products_from_csv"),
29
+ "import_products_from_urls": ("shelfshift.core.importers.url", "import_products_from_urls"),
30
+ "import_url": ("shelfshift.core.api", "import_url"),
31
+ "list_exporters": ("shelfshift.core.registry", "list_exporters"),
32
+ "list_importers": ("shelfshift.core.registry", "list_importers"),
33
+ "parse_product_payload": ("shelfshift.core.api", "parse_product_payload"),
34
+ "register_exporter": ("shelfshift.core.registry", "register_exporter"),
35
+ "register_importer": ("shelfshift.core.registry", "register_importer"),
36
+ "validate": ("shelfshift.core.api", "validate"),
37
+ }
38
+
39
+ __all__ = [
40
+ "CoreConfig",
41
+ "DetectResult",
42
+ "ExportResult",
43
+ "ImportResult",
44
+ "Product",
45
+ "config_from_env",
46
+ "convert_csv",
47
+ "detect_csv",
48
+ "detect_csv_platform",
49
+ "detect_url",
50
+ "detect_product_url",
51
+ "export_csv",
52
+ "export_csv_for_target",
53
+ "get_exporter",
54
+ "get_importer",
55
+ "import_csv",
56
+ "import_product_from_csv",
57
+ "import_product_from_url",
58
+ "import_products_from_csv",
59
+ "import_products_from_urls",
60
+ "import_url",
61
+ "list_exporters",
62
+ "list_importers",
63
+ "parse_product_payload",
64
+ "register_exporter",
65
+ "register_importer",
66
+ "validate",
67
+ ]
68
+
69
+
70
+ def __getattr__(name: str) -> Any:
71
+ target = _LAZY_EXPORTS.get(name)
72
+ if target is None:
73
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
74
+
75
+ module_name, attribute_name = target
76
+ module = __import__(module_name, fromlist=[attribute_name])
77
+ value = getattr(module, attribute_name)
78
+ globals()[name] = value
79
+ return value
shelfshift/core/api.py ADDED
@@ -0,0 +1,227 @@
1
+ """Stable public API facade for the Shelfshift core engine."""
2
+
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .canonical.entities import Product
9
+ from .config import CoreConfig, config_from_env, resolve_rapidapi_key
10
+ from .detect import detect_csv_platform as _detect_csv_platform
11
+ from .detect import detect_product_url as _detect_product_url
12
+ from .importers.csv.common import parse_canonical_product_payload
13
+ from .importers.csv import import_product_from_csv, import_products_from_csv
14
+ from .importers.url import import_product_from_url, import_products_from_urls
15
+ from .registry import get_exporter
16
+ from .validate import ValidationReport, validate_product
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DetectResult:
21
+ kind: str
22
+ platform: str | None
23
+ is_product: bool
24
+ product_id: str | None = None
25
+ slug: str | None = None
26
+
27
+
28
+ @dataclass
29
+ class ImportResult:
30
+ products: list[Product] = field(default_factory=list)
31
+ errors: list[dict[str, str]] = field(default_factory=list)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ExportResult:
36
+ csv_bytes: bytes
37
+ filename: str
38
+
39
+
40
+ def detect_url(url: str) -> DetectResult:
41
+ payload = _detect_product_url(url)
42
+ return DetectResult(
43
+ kind="url",
44
+ platform=payload.get("platform"),
45
+ is_product=bool(payload.get("is_product", False)),
46
+ product_id=payload.get("product_id"),
47
+ slug=payload.get("slug"),
48
+ )
49
+
50
+
51
+ def detect_csv(csv_input: bytes | str | Path) -> DetectResult:
52
+ csv_bytes = _coerce_bytes(csv_input)
53
+ platform = _detect_csv_platform(csv_bytes)
54
+ return DetectResult(kind="csv", platform=platform, is_product=False)
55
+
56
+
57
+ def import_url(
58
+ urls: str | list[str],
59
+ *,
60
+ strict: bool = False,
61
+ debug: bool = False,
62
+ rapidapi_key: str | None = None,
63
+ ) -> ImportResult:
64
+ config = config_from_env(strict=strict, debug=debug)
65
+ resolved_rapidapi_key = resolve_rapidapi_key(rapidapi_key)
66
+
67
+ if isinstance(urls, str):
68
+ product = import_product_from_url(urls, rapidapi_key=resolved_rapidapi_key)
69
+ return ImportResult(products=[product], errors=[])
70
+
71
+ products, errors = import_products_from_urls(
72
+ list(urls),
73
+ rapidapi_key=resolved_rapidapi_key,
74
+ )
75
+ if config.strict and errors:
76
+ raise ValueError(f"Strict mode failed with {len(errors)} URL import error(s).")
77
+ return ImportResult(products=products, errors=errors)
78
+
79
+
80
+ def import_csv(
81
+ csv_input: bytes | str | Path,
82
+ *,
83
+ platform: str | None = None,
84
+ strict: bool = False,
85
+ source_weight_unit: str | None = None,
86
+ ) -> ImportResult:
87
+ csv_bytes = _coerce_bytes(csv_input)
88
+ source_platform = (platform or _detect_csv_platform(csv_bytes)).strip().lower()
89
+ products = import_products_from_csv(
90
+ source_platform=source_platform,
91
+ csv_bytes=csv_bytes,
92
+ source_weight_unit=source_weight_unit,
93
+ )
94
+ if strict and not products:
95
+ raise ValueError("Strict mode failed: no products imported from CSV.")
96
+ return ImportResult(products=products, errors=[])
97
+
98
+
99
+ def export_csv(
100
+ products: Product | list[Product],
101
+ *,
102
+ target: str,
103
+ options: dict[str, Any] | None = None,
104
+ ) -> ExportResult:
105
+ normalized_target = str(target).strip().lower()
106
+ opts = dict(options or {})
107
+
108
+ publish = bool(opts.get("publish", False))
109
+ weight_unit = str(opts.get("weight_unit", ""))
110
+ bigcommerce_csv_format = str(opts.get("bigcommerce_csv_format", "modern"))
111
+ squarespace_product_page = str(opts.get("squarespace_product_page", ""))
112
+ squarespace_product_url = str(opts.get("squarespace_product_url", ""))
113
+
114
+ try:
115
+ exporter = get_exporter(normalized_target)
116
+ except KeyError as exc:
117
+ raise ValueError(
118
+ "target_platform must be one of: shopify, bigcommerce, wix, squarespace, woocommerce"
119
+ ) from exc
120
+
121
+ if isinstance(products, list):
122
+ from .exporters.shared.batch import (
123
+ products_to_bigcommerce_csv,
124
+ products_to_shopify_csv,
125
+ products_to_squarespace_csv,
126
+ products_to_wix_csv,
127
+ products_to_woocommerce_csv,
128
+ )
129
+
130
+ if normalized_target == "shopify":
131
+ csv_text, filename = products_to_shopify_csv(products, publish=publish, weight_unit=weight_unit)
132
+ elif normalized_target == "bigcommerce":
133
+ csv_text, filename = products_to_bigcommerce_csv(
134
+ products,
135
+ publish=publish,
136
+ csv_format=bigcommerce_csv_format,
137
+ weight_unit=weight_unit,
138
+ )
139
+ elif normalized_target == "wix":
140
+ csv_text, filename = products_to_wix_csv(products, publish=publish, weight_unit=weight_unit)
141
+ elif normalized_target == "squarespace":
142
+ csv_text, filename = products_to_squarespace_csv(
143
+ products,
144
+ publish=publish,
145
+ product_page=squarespace_product_page,
146
+ product_url=squarespace_product_url,
147
+ weight_unit=weight_unit,
148
+ )
149
+ elif normalized_target == "woocommerce":
150
+ csv_text, filename = products_to_woocommerce_csv(products, publish=publish, weight_unit=weight_unit)
151
+ else:
152
+ raise ValueError(f"Unsupported target platform: {normalized_target}")
153
+ return ExportResult(csv_bytes=csv_text.encode("utf-8"), filename=filename)
154
+
155
+ csv_text, filename = exporter(
156
+ products,
157
+ target_platform=normalized_target,
158
+ publish=publish,
159
+ weight_unit=weight_unit,
160
+ bigcommerce_csv_format=bigcommerce_csv_format,
161
+ squarespace_product_page=squarespace_product_page,
162
+ squarespace_product_url=squarespace_product_url,
163
+ )
164
+ return ExportResult(csv_bytes=csv_text.encode("utf-8"), filename=filename)
165
+
166
+
167
+ def convert_csv(
168
+ csv_input: bytes | str | Path,
169
+ *,
170
+ target: str,
171
+ source: str | None = None,
172
+ strict: bool = False,
173
+ source_weight_unit: str | None = None,
174
+ export_options: dict[str, Any] | None = None,
175
+ ) -> tuple[bytes, dict[str, Any]]:
176
+ imported = import_csv(
177
+ csv_input,
178
+ platform=source,
179
+ strict=strict,
180
+ source_weight_unit=source_weight_unit,
181
+ )
182
+ exported = export_csv(imported.products, target=target, options=export_options)
183
+
184
+ report = {
185
+ "source_platform": source or _detect_csv_platform(_coerce_bytes(csv_input)),
186
+ "target_platform": str(target).strip().lower(),
187
+ "product_count": len(imported.products),
188
+ "errors": imported.errors,
189
+ "filename": exported.filename,
190
+ }
191
+ return exported.csv_bytes, report
192
+
193
+
194
+ def validate(products: Product | list[Product]) -> list[ValidationReport]:
195
+ if isinstance(products, list):
196
+ return [validate_product(product) for product in products]
197
+ return [validate_product(products)]
198
+
199
+
200
+ def parse_product_payload(payload: dict[str, Any] | list[dict[str, Any]]) -> Product | list[Product]:
201
+ if isinstance(payload, list):
202
+ return [parse_canonical_product_payload(item) for item in payload]
203
+ return parse_canonical_product_payload(payload)
204
+
205
+
206
+ def _coerce_bytes(value: bytes | str | Path) -> bytes:
207
+ if isinstance(value, bytes):
208
+ return value
209
+ path = Path(value)
210
+ return path.read_bytes()
211
+
212
+
213
+ __all__ = [
214
+ "CoreConfig",
215
+ "DetectResult",
216
+ "ExportResult",
217
+ "ImportResult",
218
+ "config_from_env",
219
+ "convert_csv",
220
+ "detect_csv",
221
+ "detect_url",
222
+ "export_csv",
223
+ "import_csv",
224
+ "import_url",
225
+ "parse_product_payload",
226
+ "validate",
227
+ ]
@@ -0,0 +1,60 @@
1
+ from .entities import (
2
+ CategorySet,
3
+ Currency,
4
+ Identifiers,
5
+ Inventory,
6
+ Media,
7
+ MediaType,
8
+ Money,
9
+ OptionDef,
10
+ OptionValue,
11
+ Price,
12
+ Product,
13
+ Seo,
14
+ SourceRef,
15
+ Variant,
16
+ Weight,
17
+ WeightUnit,
18
+ )
19
+ from .helpers import (
20
+ format_decimal,
21
+ normalize_currency,
22
+ parse_decimal_money,
23
+ resolve_all_image_urls,
24
+ resolve_current_money,
25
+ resolve_option_defs,
26
+ resolve_primary_image_url,
27
+ resolve_taxonomy_paths,
28
+ resolve_variant_option_values,
29
+ )
30
+ from .serialization import serialize_product_for_api, serialize_variant_for_api
31
+
32
+ __all__ = [
33
+ "Currency",
34
+ "CategorySet",
35
+ "Identifiers",
36
+ "Inventory",
37
+ "Media",
38
+ "MediaType",
39
+ "Money",
40
+ "OptionDef",
41
+ "OptionValue",
42
+ "Price",
43
+ "Product",
44
+ "Seo",
45
+ "SourceRef",
46
+ "Variant",
47
+ "Weight",
48
+ "WeightUnit",
49
+ "format_decimal",
50
+ "normalize_currency",
51
+ "parse_decimal_money",
52
+ "resolve_all_image_urls",
53
+ "resolve_current_money",
54
+ "resolve_option_defs",
55
+ "resolve_primary_image_url",
56
+ "resolve_taxonomy_paths",
57
+ "resolve_variant_option_values",
58
+ "serialize_product_for_api",
59
+ "serialize_variant_for_api",
60
+ ]