From c5703bbb8846a5433207744e6ada75057cf96b25 Mon Sep 17 00:00:00 2001 From: husanjon Date: Thu, 2 Apr 2026 12:10:42 +0500 Subject: [PATCH] category va elonlanri olish optimalashtrildi --- sync_firebase_products.py | 125 +++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 35 deletions(-) diff --git a/sync_firebase_products.py b/sync_firebase_products.py index 004e5be..4543403 100644 --- a/sync_firebase_products.py +++ b/sync_firebase_products.py @@ -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,18 +201,21 @@ 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}") - print(f"Yangilandi: {updated}") + if update: + print(f"Yangilandi: {updated}") print(f"O'tkazildi: {skipped}") - print(f"Xatolar : {errors}") + 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)