first deployment of kikomiko with url photos

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shayan khoshrou
2026-10-19 19:10:57 +02:00
co-authored by Cursor
parent 8d98304137
commit ae3ed7c7b7
159 changed files with 59509 additions and 0 deletions
+52
View File
@@ -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}")
+84
View File
@@ -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
+71
View File
@@ -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