Files
evo-sync/web/tasks/vk_catalog.py
mguschin 796cf49ff9 feat: Evotor + VK catalog sync, connections, and store/group filters
- 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>
2026-05-01 18:09:11 +03:00

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