Spaces:
Sleeping
Sleeping
File size: 16,826 Bytes
b2be963 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 | from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import text
import json
import random
import os
import hashlib
import requests
from app.db.base import get_db
from app.models.product import Product, ProductImage, Category
from app.models.cart import Coupon
from app.models.user import User, UserRole, AuthProvider
from app.core.security import hash_password
from app.schemas.auth import AuthResponse
from app.services.external_catalog import ExternalCatalogService
router = APIRouter(prefix="/seed", tags=["Seed Data"])
# ============================================================
# Category Hierarchy — matching Extra_Scraper_Kaggle.py exactly
# ============================================================
SUB_ICONS = {
"جوالات": "📲",
"لابتوب": "💻",
"أجهزة لوحية": "📟",
"تلفزيونات": "📺",
"كاميرات": "📷",
"طابعات": "🖨️",
"أجهزة الصوت والسماعات": "🎧",
"ساعات ذكية": "⌚",
"أجهزة ألعاب": "🎮",
"ثلاجات": "❄️",
"غسالات ومجففات": "🫧",
"مكيفات": "🌀",
"أجهزة منزلية صغيرة": "🍳",
"أجهزة منزلية كبيرة": "🏗️",
}
CATEGORY_HIERARCHY = {
"الأجهزة الإلكترونية": {
"name_en": "Electronics",
"icon": "📱",
"subs": [
{"ar": "جوالات", "en": "Smartphones", "icon": "📲"},
{"ar": "لابتوب", "en": "Laptops", "icon": "💻"},
{"ar": "أجهزة لوحية", "en": "Tablets", "icon": "📟"},
{"ar": "تلفزيونات", "en": "TVs", "icon": "📺"},
{"ar": "كاميرات", "en": "Cameras", "icon": "📷"},
{"ar": "طابعات", "en": "Printers", "icon": "🖨️"},
]
},
"ملحقات واكسسوارات": {
"name_en": "Accessories",
"icon": "🔌",
"subs": [
{"ar": "أجهزة الصوت والسماعات", "en": "Audio & Headphones", "icon": "🎧"},
{"ar": "ساعات ذكية", "en": "Smartwatches", "icon": "⌚"},
]
},
"ألعاب جيمنج": {
"name_en": "Gaming",
"icon": "🎮",
"subs": [
{"ar": "أجهزة ألعاب", "en": "Gaming Consoles", "icon": "🎮"},
]
},
"الأجهزة المنزلية": {
"name_en": "Home Appliances",
"icon": "🏠",
"subs": [
{"ar": "ثلاجات", "en": "Refrigerators", "icon": "❄️"},
{"ar": "غسالات ومجففات", "en": "Washing Machines", "icon": "🫧"},
{"ar": "مكيفات", "en": "Air Conditioners", "icon": "🌀"},
{"ar": "أجهزة منزلية صغيرة", "en": "Small Appliances", "icon": "🍳"},
{"ar": "أجهزة منزلية كبيرة", "en": "Large Appliances", "icon": "🏗️"},
]
}
}
# Reverse map for scraped data → parent category
SCRAPED_TO_TARGET_MAP = {
"Electronics": "الأجهزة الإلكترونية",
"Smartphones": "الأجهزة الإلكترونية",
"Laptops": "الأجهزة الإلكترونية",
"Tablets": "الأجهزة الإلكترونية",
"TVs": "الأجهزة الإلكترونية",
"Cameras": "الأجهزة الإلكترونية",
"Printers": "الأجهزة الإلكترونية",
"Accessories": "ملحقات واكسسوارات",
"Audio & Headphones": "ملحقات واكسسوارات",
"Smartwatches": "ملحقات واكسسوارات",
"Gaming": "ألعاب جيمنج",
"Gaming Consoles": "ألعاب جيمنج",
"Home Appliances": "الأجهزة المنزلية",
"Refrigerators": "الأجهزة المنزلية",
"Washing Machines": "الأجهزة المنزلية",
"Air Conditioners": "الأجهزة المنزلية",
"Small Appliances": "الأجهزة المنزلية",
"Large Appliances": "الأجهزة المنزلية",
"Security": "ملحقات واكسسوارات",
"Car": "ملحقات واكسسوارات",
"Audio": "ملحقات واكسسوارات",
"Personal Care": "الأجهزة المنزلية",
}
@router.api_route("", methods=["GET", "POST"], response_model=AuthResponse)
def seed_database(db: Session = Depends(get_db), is_background_task: bool = False):
"""Populate database with proper product data. Only adds new products, does not clear existing data."""
if is_background_task:
from app.core.state import import_progress
# 0. Ensure Store Settings exist
from app.models.settings import StoreSettings
settings = db.query(StoreSettings).first()
if not settings:
print(">>> [SEED] Creating default store settings...")
settings = StoreSettings(
store_name="أفق",
primary_color="#046c4e",
secondary_color="#d97706",
logo_url="/logo.png"
)
db.add(settings)
db.commit()
# 0. Create Default Admin if matches criteria
admin_email = "admin@vortex.com"
admin_user = db.query(User).filter(User.email == admin_email).first()
if not admin_user:
print(f">>> [SEED] Creating default admin: {admin_email}")
admin_user = User(
email=admin_email,
name="Platform Administrator",
password_hash=hash_password("admin123"),
role=UserRole.ADMIN,
auth_provider=AuthProvider.LOCAL
)
db.add(admin_user)
db.commit()
# 1. Create Categories (Hierarchy)
all_sub_cats = {} # Map used for product assignment
for p_ar, info in CATEGORY_HIERARCHY.items():
# Create Parent
parent_cat = db.query(Category).filter(Category.name_ar == p_ar).first()
if not parent_cat:
parent_cat = Category(
name_ar=p_ar,
name_en=info["name_en"],
icon=info["icon"]
)
db.add(parent_cat)
db.commit()
db.refresh(parent_cat)
# Add parent to map so products can be assigned directly to it
all_sub_cats[p_ar] = parent_cat
all_sub_cats[info["name_en"]] = parent_cat
# Create Subs
for s in info["subs"]:
sub_cat = db.query(Category).filter(Category.name_ar == s["ar"]).first()
if not sub_cat:
sub_cat = Category(
name_ar=s["ar"],
name_en=s["en"],
icon=s.get("icon", SUB_ICONS.get(s["ar"], "📦")),
parent_id=parent_cat.id
)
db.add(sub_cat)
db.commit()
db.refresh(sub_cat)
all_sub_cats[s["ar"]] = sub_cat
all_sub_cats[s["en"]] = sub_cat # Map both names to the same object
API_VERSION = "2026.03.16.V2"
possible_paths = [
# Primary Target (Fixed location inside backend so upload_to_hf works)
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "data", "extra_products_vortex.json")),
os.path.abspath("app/data/extra_products_vortex.json"),
# Standard Hugging Face / Docker location (WORKDIR /app)
"/app/app/data/extra_products_vortex.json",
"/app/data/extra_products_vortex.json",
# Fallbacks (Old paths just in case)
r"c:\react_projects\VortexCommerce\kaggle\extra_products_vortex.json",
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "kaggle", "extra_products_vortex.json")),
os.path.abspath("kaggle/extra_products_vortex.json"),
os.path.abspath("extra_products_vortex.json"),
]
json_path = None
checked_info = []
for p in possible_paths:
exists = os.path.exists(p)
checked_info.append(f"{p}: {'FOUND' if exists else 'NOT FOUND'}")
if exists:
json_path = p
break
if not json_path:
return AuthResponse(
isSuccess=False,
value={
"message": "Source file not found.",
"checked": checked_info,
"version": API_VERSION
},
statusCode=404,
error="File not found"
)
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and "products" in data:
products_data = data["products"]
elif isinstance(data, list):
products_data = data
else:
return AuthResponse(isSuccess=False, value={"message": "Invalid JSON format"}, statusCode=400)
# Cache existing products
existing_products_ar = set(r[0] for r in db.execute(text("SELECT name_ar FROM products")).fetchall())
existing_products_en = set(r[0] for r in db.execute(text("SELECT name_en FROM products")).fetchall())
# 3. Create Products
inserted_count: int = 0
skipped_count: int = 0
total_items = len(products_data)
if is_background_task:
import_progress.total = total_items
import_progress.current = 0
for idx, item in enumerate(products_data):
if is_background_task and idx % 25 == 0:
import_progress.current = idx
import_progress.message = f"Processing item {idx} of {total_items}..."
if not isinstance(item, dict):
skipped_count += 1
continue
product_name_ar = item.get("name_ar", "").strip()
product_name_en = item.get("name_en", "").strip()
# 1. Resolve Category (Hierarchical)
main_cat_ar = item.get("main_category")
main_cat_en = item.get("main_category_en")
sub_cat_ar = item.get("category")
sub_cat_en = item.get("category_en")
cat_icon = item.get("category_icon", "📦")
target_sub_cat = None
if main_cat_ar and sub_cat_ar:
# Ensure Parent exists
parent = db.query(Category).filter(Category.name_ar == main_cat_ar).first()
if not parent:
parent = Category(
name_ar=main_cat_ar,
name_en=main_cat_en or main_cat_ar,
icon=cat_icon
)
db.add(parent)
db.commit()
db.refresh(parent)
# Ensure Sub exists
sub = db.query(Category).filter(
Category.name_ar == sub_cat_ar,
Category.parent_id == parent.id
).first()
if not sub:
sub = Category(
name_ar=sub_cat_ar,
name_en=sub_cat_en or sub_cat_ar,
icon=cat_icon,
parent_id=parent.id
)
db.add(sub)
db.commit()
db.refresh(sub)
target_sub_cat = sub
else:
# Fallback for old flat format or missing data
cat_name = item.get("category_name") or item.get("category")
if cat_name in SCRAPED_TO_TARGET_MAP:
cat_name = SCRAPED_TO_TARGET_MAP[cat_name]
if not cat_name or cat_name not in all_sub_cats:
cat_name = "الأجهزة الإلكترونية"
target_sub_cat = all_sub_cats.get(cat_name)
if not target_sub_cat:
skipped_count += 1
continue
if product_name_ar in existing_products_ar or product_name_en in existing_products_en:
skipped_count += 1
continue
specs = item.get("specs", {})
if not isinstance(specs, dict): specs = {}
# Handle Variants
if "variants" not in specs and "options" in item:
options = item.get("options", [])
if isinstance(options, list):
variants = []
for opt in options:
if not isinstance(opt, dict): continue
variants.append({
"id": opt.get("sku", opt.get("id", "v_default")),
"name_en": opt.get("name_en", ""),
"name_ar": opt.get("name_ar", ""),
"price_modifier": float(opt.get("price", 0)) - float(item.get("price", 0)) if "price" in opt else 0,
"stock": opt.get("stock", 10),
"image_url": opt.get("image", opt.get("image_url", ""))
})
if variants: specs["variants"] = variants
import time
fallback_slug = f"vortex-{int(time.time()*1000)}-{idx}"
product_slug = item.get("legacy_ref") or item.get("sku_base") or fallback_slug
prod = Product(
slug=product_slug,
name_ar=product_name_ar,
name_en=product_name_en,
description_ar=item.get("description_ar", ""),
description_en=item.get("description_en", ""),
price=float(item.get("price", 0)),
compare_price=round(float(item.get("compare_price", float(item.get("price", 0)) * 1.3)), 2),
stock=item.get("stock", random.randint(10, 100)),
category_id=target_sub_cat.id,
rating=item.get("rating", 4.5),
rating_count=item.get("rating_count", 20),
is_featured=item.get("is_featured", False),
is_active=item.get("is_active", True),
specs=specs if specs else None
)
db.add(prod)
db.flush()
# Handle Images
for i, img_url in enumerate(item.get("images", [])):
# Sanitize URL: Remove complex query parameters that might cause loading issues
# Extra.com URLs often look like: ...Black?locale=en-GB,en-*,*&$Listing-Product-2x$
# We want to keep the base image but simplify the request
clean_url = img_url
if "media.extra.com" in img_url and "?" in img_url:
clean_url = img_url.split("?")[0]
db.add(ProductImage(
product_id=prod.id,
image_url=clean_url,
alt_text=prod.name_ar,
sort_order=i
))
inserted_count = inserted_count + 1
existing_products_ar.add(product_name_ar)
existing_products_en.add(product_name_en)
# Commit every 20 products for real-time progress and stability
if inserted_count % 20 == 0:
db.commit()
print(f">>> [SEED] Progress: {inserted_count} products committed...")
# Ensure Demo Coupons
for code, discount in [("WELCOME10", 10.0), ("VORTEX20", 20.0)]:
if not db.query(Coupon).filter(Coupon.code == code).first():
db.add(Coupon(code=code, discount_percent=discount, is_active=True))
db.commit()
print(f">>> [SEED] FINAL: {inserted_count} products inserted.")
return AuthResponse(
isSuccess=True,
value={
"message": "تمت عملية المزامنة بنجاح",
"total_products": len(products_data),
"inserted_new": inserted_count,
"skipped": skipped_count,
"source_file": os.path.basename(json_path),
"path": json_path,
"version": API_VERSION
},
statusCode=201,
)
except Exception as e:
db.rollback()
return AuthResponse(
isSuccess=False,
value={"message": str(e)},
statusCode=500,
error=str(e)
)
|