category va elonlanri olish optimalashtrildi
This commit is contained in:
@@ -3,22 +3,27 @@ Firebase → Django mahsulotlarni sinxronlashtirish skripti.
|
||||
|
||||
Ishlatish:
|
||||
python sync_firebase_products.py
|
||||
python sync_firebase_products.py --dry-run # haqiqatda saqlamaydi
|
||||
python sync_firebase_products.py --update # mavjudlarni ham yangilaydi
|
||||
python sync_firebase_products.py --dry-run
|
||||
python sync_firebase_products.py --update
|
||||
python sync_firebase_products.py --after "2024-01-15"
|
||||
python sync_firebase_products.py --after "2024-01-15 10:30:00"
|
||||
|
||||
Logika:
|
||||
- firestore_id bo'yicha tekshiradi
|
||||
- Mavjud bo'lsa — o'tkazib ketadi (--update bilan yangilaydi)
|
||||
- Yangi bo'lsa — create qiladi
|
||||
- --after bilan faqat o'sha vaqtdan keyin qo'shilganlarni oladi
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import django
|
||||
import firebase_admin
|
||||
from firebase_admin import credentials, firestore
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Django sozlash ───────────────────────────────────────────────────────────
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -54,7 +59,6 @@ db = firestore.client()
|
||||
# ── Yordamchi funksiyalar ────────────────────────────────────────────────────
|
||||
|
||||
def to_decimal(value, default=Decimal("0")):
|
||||
"""Har qanday qiymatni Decimal'ga o'tkazadi."""
|
||||
try:
|
||||
return Decimal(str(value)).quantize(Decimal("0.01"))
|
||||
except (InvalidOperation, TypeError):
|
||||
@@ -68,35 +72,47 @@ def to_int(value, default=-1):
|
||||
return default
|
||||
|
||||
|
||||
def parse_after_date(date_str: str) -> datetime:
|
||||
"""'2024-01-15' yoki '2024-01-15 10:30:00' ni timezone-aware datetime'ga o'tkazadi."""
|
||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
|
||||
try:
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError(
|
||||
f"Noto'g'ri sana formati: '{date_str}'\n"
|
||||
"To'g'ri formatlar: '2024-01-15' yoki '2024-01-15 10:30:00'"
|
||||
)
|
||||
|
||||
|
||||
def get_category(firestore_id: str):
|
||||
"""CategoryModel'ni firestore_id bo'yicha topadi."""
|
||||
if not firestore_id:
|
||||
return None
|
||||
return CategoryModel.objects.filter(firestore_id=firestore_id).first()
|
||||
|
||||
|
||||
def get_section(firestore_id: str):
|
||||
"""SectionModel'ni firestore_id bo'yicha topadi."""
|
||||
if not firestore_id:
|
||||
return None
|
||||
return SectionModel.objects.filter(firestore_id=firestore_id).first()
|
||||
|
||||
|
||||
def build_product_data(doc_id: str, data: dict) -> dict:
|
||||
"""Firebase hujjatidan Django model uchun dict yasaydi."""
|
||||
|
||||
# product_specification: {"Og'irlik": "1 kg", "Hajm": "500 ml"} kabi
|
||||
spec = data.get("product_specification")
|
||||
def parse_spec(spec):
|
||||
"""product_specification ni dict'ga o'tkazadi."""
|
||||
if isinstance(spec, dict) and spec:
|
||||
return spec
|
||||
if isinstance(spec, str):
|
||||
# Ba'zida JSON string sifatida keladi
|
||||
import json
|
||||
try:
|
||||
spec = json.loads(spec)
|
||||
parsed = json.loads(spec)
|
||||
if isinstance(parsed, dict) and parsed:
|
||||
return parsed
|
||||
except Exception:
|
||||
spec = None
|
||||
if not isinstance(spec, dict) or not spec:
|
||||
spec = None
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def build_product_data(doc_id: str, data: dict) -> dict:
|
||||
return dict(
|
||||
firestore_id=doc_id,
|
||||
vendor=data.get("vendorID") or data.get("vendor") or "",
|
||||
@@ -107,30 +123,52 @@ def build_product_data(doc_id: str, data: dict) -> dict:
|
||||
quantity=to_int(data.get("quantity", -1)),
|
||||
is_publish=bool(data.get("publish", True)),
|
||||
photos_json=data.get("photos") or None,
|
||||
product_specification=spec,
|
||||
# ForeignKey'lar ayrida to'ldiriladi
|
||||
product_specification=parse_spec(data.get("product_specification")),
|
||||
_category_id=data.get("categoryID") or "",
|
||||
_section_id=data.get("section_id") or "",
|
||||
)
|
||||
|
||||
|
||||
def sync_products(dry_run: bool = False, update: bool = False):
|
||||
def print_progress(current, total, created, updated, skipped, errors):
|
||||
percent = int(current / total * 100) if total else 0
|
||||
bar_filled = percent // 5
|
||||
bar = "█" * bar_filled + "░" * (20 - bar_filled)
|
||||
line = (
|
||||
f"\r [{bar}] {percent:3d}% "
|
||||
f"{current}/{total} | "
|
||||
f"✓ {created} ↑ {updated} — {skipped} ✗ {errors}"
|
||||
)
|
||||
print(line, end="", flush=True)
|
||||
|
||||
|
||||
# ── Asosiy funksiya ───────────────────────────────────────────────────────────
|
||||
|
||||
def sync_products(dry_run: bool = False, update: bool = False, after: datetime | None = None):
|
||||
print("Firebase vendor_products kolleksiyasi o'qilmoqda…")
|
||||
docs = list(db.collection("vendor_products").stream())
|
||||
print(f"Jami Firebase'da: {len(docs)} ta mahsulot\n")
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
errors = 0
|
||||
query = db.collection("vendor_products")
|
||||
if after:
|
||||
# Firestore'da createdAt field bo'yicha server tomonida filter
|
||||
query = query.where("createdAt", ">=", after)
|
||||
print(f"Filter: createdAt >= {after.strftime('%Y-%m-%d %H:%M:%S')} (UTC)")
|
||||
|
||||
for doc in docs:
|
||||
docs = list(query.stream())
|
||||
total = len(docs)
|
||||
print(f"Jami Firebase'da: {total} ta mahsulot\n")
|
||||
|
||||
if total == 0:
|
||||
print("Sinxronlash uchun mahsulot topilmadi.")
|
||||
return
|
||||
|
||||
created = updated = skipped = errors = 0
|
||||
|
||||
for i, doc in enumerate(docs, start=1):
|
||||
doc_id = doc.id
|
||||
data = doc.to_dict() or {}
|
||||
|
||||
if not data.get("name"):
|
||||
print(f" [O'TKAZILDI] {doc_id} — nomi yo'q")
|
||||
skipped += 1
|
||||
print_progress(i, total, created, updated, skipped, errors)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -142,9 +180,9 @@ def sync_products(dry_run: bool = False, update: bool = False):
|
||||
|
||||
if exists and not update:
|
||||
skipped += 1
|
||||
print_progress(i, total, created, updated, skipped, errors)
|
||||
continue
|
||||
|
||||
# ForeignKey topish
|
||||
category = get_category(category_fid)
|
||||
section = get_section(section_fid)
|
||||
|
||||
@@ -155,7 +193,6 @@ def sync_products(dry_run: bool = False, update: bool = False):
|
||||
exists.section = section
|
||||
if not dry_run:
|
||||
exists.save()
|
||||
print(f" [YANGILANDI] {doc_id} — {product_data['name']}")
|
||||
updated += 1
|
||||
else:
|
||||
if not dry_run:
|
||||
@@ -164,17 +201,20 @@ def sync_products(dry_run: bool = False, update: bool = False):
|
||||
category=category,
|
||||
section=section,
|
||||
)
|
||||
print(f" [YARATILDI] {doc_id} — {product_data['name']}")
|
||||
created += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" [XATO] {doc_id}: {e}")
|
||||
print(f"\n [XATO] {doc_id}: {e}")
|
||||
errors += 1
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print_progress(i, total, created, updated, skipped, errors)
|
||||
|
||||
print("\n\n" + "=" * 50)
|
||||
print(f"Yaratildi : {created}")
|
||||
if update:
|
||||
print(f"Yangilandi: {updated}")
|
||||
print(f"O'tkazildi: {skipped}")
|
||||
if errors:
|
||||
print(f"Xatolar : {errors}")
|
||||
if dry_run:
|
||||
print("\n[DRY-RUN] Hech narsa saqlanmadi.")
|
||||
@@ -194,6 +234,21 @@ if __name__ == "__main__":
|
||||
action="store_true",
|
||||
help="Mavjud mahsulotlarni ham yangilaydi (default: o'tkazib ketadi)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--after",
|
||||
type=str,
|
||||
default=None,
|
||||
metavar="SANA",
|
||||
help="Faqat shu vaqtdan keyin qo'shilgan mahsulotlar. Misol: '2024-01-15' yoki '2024-01-15 10:30:00'",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
sync_products(dry_run=args.dry_run, update=args.update)
|
||||
after_dt: datetime | None = None
|
||||
if args.after:
|
||||
try:
|
||||
after_dt = parse_after_date(args.after)
|
||||
except ValueError as e:
|
||||
print(f"[XATO] {e}")
|
||||
sys.exit(1)
|
||||
|
||||
sync_products(dry_run=args.dry_run, update=args.update, after=after_dt)
|
||||
|
||||
Reference in New Issue
Block a user