- Evotor catalog: background Celery task syncing stores/groups/products from Evotor API; UI pages with per-store and per-group sync toggles - VK connection: manual token + group ID entry with inline test button - Evotor connection: inline test button (calls /stores) - VK catalog: background task syncing VK Market albums and products; separate catalog UI at /vk-catalog/albums - SyncFilter extended to support entity_type=group with parent_entity_id - Migration 0004: vk_cached_albums + vk_cached_products tables - Beat schedule updated to run both refresh_catalog and refresh_vk_catalog - README updated with new schema, routes, tasks, and config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
5.5 KiB
Python
168 lines
5.5 KiB
Python
"""
|
|
Periodic VK catalog sync: fetch albums and products from VK Market
|
|
for every connected user and upsert into vk_cached_* tables.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
import httpx
|
|
from celery import shared_task
|
|
|
|
from web.config import settings
|
|
from web.database import SessionLocal
|
|
from web.models.connections import VkCachedAlbum, VkCachedProduct, VkConnection
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
VK_API = "https://api.vk.com/method"
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
|
|
|
|
def _vk_get(method: str, params: dict, token: str) -> dict:
|
|
params = {**params, "access_token": token, "v": settings.VK_API_VERSION}
|
|
r = httpx.get(f"{VK_API}/{method}", params=params, timeout=20)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _sync_user(db, user_id: int, token: str, group_id: str) -> None:
|
|
now = _now()
|
|
owner_id = f"-{group_id}"
|
|
|
|
# ── albums ────────────────────────────────────────────────────────────────
|
|
try:
|
|
data = _vk_get("market.getAlbums", {"owner_id": owner_id, "count": 100}, token)
|
|
except Exception as e:
|
|
logger.warning("user=%s vk fetch albums failed: %s", user_id, e)
|
|
return
|
|
|
|
if "error" in data:
|
|
logger.warning("user=%s vk albums error: %s", user_id, data["error"])
|
|
return
|
|
|
|
albums = data.get("response", {}).get("items", [])
|
|
album_ids = []
|
|
for a in albums:
|
|
aid = str(a["id"])
|
|
album_ids.append(aid)
|
|
row = db.query(VkCachedAlbum).filter_by(user_id=user_id, vk_group_id=group_id, album_id=aid).first()
|
|
if row:
|
|
row.title = a.get("title", "")
|
|
row.count = a.get("count")
|
|
row.fetched_at = now
|
|
else:
|
|
db.add(VkCachedAlbum(
|
|
user_id=user_id,
|
|
vk_group_id=group_id,
|
|
album_id=aid,
|
|
title=a.get("title", ""),
|
|
count=a.get("count"),
|
|
fetched_at=now,
|
|
))
|
|
db.flush()
|
|
|
|
# ── products (extended=1 gives albums_ids per product) ───────────────────
|
|
offset = 0
|
|
all_products = []
|
|
while True:
|
|
try:
|
|
data = _vk_get(
|
|
"market.get",
|
|
{"owner_id": owner_id, "count": 200, "offset": offset, "extended": 1},
|
|
token,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("user=%s vk fetch products (extended) failed: %s", user_id, e)
|
|
break
|
|
|
|
if "error" in data:
|
|
logger.warning("user=%s vk products (extended) error: %s", user_id, data["error"])
|
|
break
|
|
|
|
items = data.get("response", {}).get("items", [])
|
|
all_products.extend(items)
|
|
if len(items) < 200:
|
|
break
|
|
offset += 200
|
|
|
|
for p in all_products:
|
|
pid = str(p["id"])
|
|
album_id = str(p["albums_ids"][0]) if p.get("albums_ids") else None
|
|
price_raw = p.get("price", {}).get("amount")
|
|
price = float(price_raw) / 100 if price_raw is not None else None
|
|
thumb = None
|
|
if p.get("thumb_photo"):
|
|
sizes = p["thumb_photo"].get("sizes", [])
|
|
if sizes:
|
|
thumb = sizes[-1].get("url")
|
|
|
|
row = db.query(VkCachedProduct).filter_by(
|
|
user_id=user_id, vk_group_id=group_id, vk_product_id=pid,
|
|
).first()
|
|
if row:
|
|
row.album_id = album_id
|
|
row.name = p.get("title", "")
|
|
row.description = p.get("description")
|
|
row.price = price
|
|
row.availability = p.get("availability")
|
|
row.thumb_url = thumb
|
|
row.fetched_at = now
|
|
else:
|
|
db.add(VkCachedProduct(
|
|
user_id=user_id,
|
|
vk_group_id=group_id,
|
|
vk_product_id=pid,
|
|
album_id=album_id,
|
|
name=p.get("title", ""),
|
|
description=p.get("description"),
|
|
price=price,
|
|
availability=p.get("availability"),
|
|
thumb_url=thumb,
|
|
fetched_at=now,
|
|
))
|
|
|
|
db.commit()
|
|
logger.info(
|
|
"user=%s vk catalog synced: group=%s albums=%d products=%d",
|
|
user_id, group_id, len(albums), len(all_products),
|
|
)
|
|
|
|
|
|
@shared_task(
|
|
name="web.tasks.vk_catalog.refresh_vk_catalog",
|
|
queue="default",
|
|
bind=True,
|
|
max_retries=2,
|
|
default_retry_delay=60,
|
|
)
|
|
def refresh_vk_catalog(self) -> dict:
|
|
"""Fetch and cache VK Market albums and products for all connected users."""
|
|
db = SessionLocal()
|
|
results = {"ok": 0, "failed": 0}
|
|
try:
|
|
connections = (
|
|
db.query(VkConnection)
|
|
.filter(
|
|
VkConnection.user_id.isnot(None),
|
|
VkConnection.access_token.isnot(None),
|
|
VkConnection.access_token != "",
|
|
VkConnection.vk_user_id.isnot(None),
|
|
VkConnection.vk_user_id != "",
|
|
)
|
|
.all()
|
|
)
|
|
for conn in connections:
|
|
try:
|
|
_sync_user(db, conn.user_id, conn.access_token, conn.vk_user_id)
|
|
results["ok"] += 1
|
|
except Exception as exc:
|
|
logger.error("vk catalog sync failed for user=%s: %s", conn.user_id, exc)
|
|
results["failed"] += 1
|
|
finally:
|
|
db.close()
|
|
logger.info("refresh_vk_catalog done: %s", results)
|
|
return results
|