first deployment of kikomiko with url photos
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8d98304137
commit
ae3ed7c7b7
@@ -0,0 +1,52 @@
|
||||
"""Pluggable product fetch / stock-sync scripts for the admin CMS.
|
||||
|
||||
Each fetcher module must expose:
|
||||
NAME: str
|
||||
DESCRIPTION: str
|
||||
fetch_products(config: dict) -> list[dict]
|
||||
sync_stocks(config: dict, products: list[dict]) -> list[dict]
|
||||
|
||||
Product dict keys (camelCase or snake_case accepted by importer):
|
||||
id, slug, name, nameFa/name_fa, price, compareAtPrice/compare_at_price,
|
||||
stock, heroImage/hero_image, category, categoryFa/category_fa,
|
||||
description, sku, externalId/external_id, colorVariants/color_variants,
|
||||
source
|
||||
|
||||
Each colorVariants item may include:
|
||||
id, slug, name, shade, colorHex, price, compareAtPrice, stock,
|
||||
heroImage, sku, externalId, images[]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
_PACKAGE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def list_fetchers():
|
||||
items = []
|
||||
for mod in pkgutil.iter_modules([str(_PACKAGE)]):
|
||||
if mod.name.startswith("_"):
|
||||
continue
|
||||
module = importlib.import_module(f"fetchers.{mod.name}")
|
||||
if not hasattr(module, "NAME"):
|
||||
continue
|
||||
items.append(
|
||||
{
|
||||
"id": mod.name,
|
||||
"name": getattr(module, "NAME"),
|
||||
"description": getattr(module, "DESCRIPTION", ""),
|
||||
"supports_fetch": callable(getattr(module, "fetch_products", None)),
|
||||
"supports_sync": callable(getattr(module, "sync_stocks", None)),
|
||||
}
|
||||
)
|
||||
return sorted(items, key=lambda x: x["id"])
|
||||
|
||||
|
||||
def get_fetcher(fetcher_id: str):
|
||||
if not fetcher_id or "/" in fetcher_id or ".." in fetcher_id:
|
||||
raise ValueError("invalid fetcher id")
|
||||
return importlib.import_module(f"fetchers.{fetcher_id}")
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Example fetcher: read products / stock from a local or remote JSON file.
|
||||
|
||||
Config (JSON in admin UI):
|
||||
{
|
||||
"url": "https://example.com/products.json", // or omit for local path
|
||||
"path": "/absolute/or/relative/products.json",
|
||||
"stock_only": false
|
||||
}
|
||||
|
||||
Expected JSON shapes:
|
||||
{ "products": [ {...}, ... ] }
|
||||
or a bare array of products.
|
||||
|
||||
For stock sync, each item needs at least: id|externalId|sku|slug and stock.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
NAME = "Example JSON source"
|
||||
DESCRIPTION = (
|
||||
"Import products or sync stock from a JSON URL/path. "
|
||||
"Replace this module with a real site-specific scraper when ready."
|
||||
)
|
||||
|
||||
|
||||
def _load_payload(config: dict):
|
||||
url = (config or {}).get("url")
|
||||
path = (config or {}).get("path")
|
||||
if url:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={"User-Agent": "KikomikoAdmin/1.0"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
if path:
|
||||
p = Path(path).expanduser()
|
||||
if not p.is_file():
|
||||
raise FileNotFoundError(f"JSON path not found: {p}")
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
raise ValueError("config needs url or path")
|
||||
|
||||
|
||||
def _as_list(payload):
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
if isinstance(payload, dict):
|
||||
for key in ("products", "items", "data"):
|
||||
if isinstance(payload.get(key), list):
|
||||
return payload[key]
|
||||
raise ValueError("JSON must be a product array or {products:[...]}")
|
||||
|
||||
|
||||
def fetch_products(config: dict):
|
||||
rows = _as_list(_load_payload(config))
|
||||
for row in rows:
|
||||
row.setdefault("source", "example_json")
|
||||
return rows
|
||||
|
||||
|
||||
def sync_stocks(config: dict, products: list):
|
||||
"""Return [{id|external_id|sku|slug, stock}, ...] for matching DB rows."""
|
||||
rows = _as_list(_load_payload(config))
|
||||
out = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
stock = row.get("stock")
|
||||
if stock is None:
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"id": row.get("id"),
|
||||
"external_id": row.get("externalId") or row.get("external_id"),
|
||||
"sku": row.get("sku"),
|
||||
"slug": row.get("slug"),
|
||||
"stock": int(stock),
|
||||
}
|
||||
)
|
||||
return out
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Fetcher: pull one or more products from kikocosmetics.com product URLs.
|
||||
|
||||
Config JSON in admin Fetch panel:
|
||||
{
|
||||
"urls": [
|
||||
"https://www.kikocosmetics.com/en-gb/p/love-fusion-24h-moisture-radiant-foundation-1-52012/"
|
||||
],
|
||||
"market": "en-gb"
|
||||
}
|
||||
|
||||
Or a single "url" / "query" for one product.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from kiko_enrich import fetch_kiko_product
|
||||
|
||||
NAME = "KIKO Cosmetics (official site)"
|
||||
DESCRIPTION = (
|
||||
"Fetch product description, details table and color variants from kikocosmetics.com. "
|
||||
"Upserts by sku/name — does not invent duplicate families."
|
||||
)
|
||||
|
||||
|
||||
def fetch_products(config: dict):
|
||||
config = config or {}
|
||||
market = config.get("market") or "en-gb"
|
||||
urls = config.get("urls")
|
||||
rows = []
|
||||
if isinstance(urls, list) and urls:
|
||||
for url in urls:
|
||||
if not url:
|
||||
continue
|
||||
rows.append(fetch_kiko_product(url=str(url), market=market))
|
||||
return rows
|
||||
product = fetch_kiko_product(
|
||||
url=config.get("url"),
|
||||
query=config.get("query") or config.get("name"),
|
||||
market=market,
|
||||
)
|
||||
return [product]
|
||||
|
||||
|
||||
def sync_stocks(config: dict, products: list):
|
||||
"""Refresh sellable flags from KIKO for products that already have sourceUrl/sku."""
|
||||
config = config or {}
|
||||
market = config.get("market") or "en-gb"
|
||||
out = []
|
||||
for p in products or []:
|
||||
url = p.get("sourceUrl")
|
||||
sku = p.get("sku")
|
||||
if not url and not sku:
|
||||
continue
|
||||
try:
|
||||
fetched = fetch_kiko_product(
|
||||
url=url,
|
||||
query=p.get("name") if not url else None,
|
||||
market=market,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"id": p.get("id"),
|
||||
"sku": fetched.get("sku") or sku,
|
||||
"external_id": fetched.get("externalId") or p.get("externalId"),
|
||||
"slug": p.get("slug"),
|
||||
"stock": int(fetched.get("stock") or 0),
|
||||
}
|
||||
)
|
||||
return out
|
||||
Reference in New Issue
Block a user