feat: /api/product/lookup endpoint eklendi

Ne yaptık:
- product_lookup_cache (BoundedCache 500 item, 1h TTL) eklendi
- IP rate limiter: 10 istek/IP/dakika
- _TRENDYOL_URL_RE regex ile URL'den ürün ID'si çekme
- GET /api/product/lookup endpoint: orderCount/favoriteCount/pageViewCount/basketCount döner
- Circuit breaker ve cache entegrasyonu

Neden yaptık:
- "Ne Kadar Sattı?" sayfası için gerekli Python servisi altyapısı.
  Kullanıcı Trendyol URL'si yapıştırıyor, sosyal kanıt verisi alıyor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
furkanyigit34
2026-04-16 12:35:45 +03:00
parent 942c8d1244
commit 982ad96f3f

View File

@@ -502,6 +502,21 @@ social_proof_cache = BoundedCache(maxsize=100, ttl=3600)
scraping_progress = BoundedCache(maxsize=50, ttl=7200)
dashboard_cache = BoundedCache(maxsize=50, ttl=3600)
enrichment_progress = BoundedCache(maxsize=50, ttl=7200)
product_lookup_cache = BoundedCache(maxsize=500, ttl=3600)
# IP rate limiter for public product lookup
_product_lookup_ip_cache: dict = {}
_product_lookup_ip_lock = Lock()
_PRODUCT_LOOKUP_MAX = 10 # per IP per minute
_TRENDYOL_URL_RE = re.compile(r'trendyol\.com/.*?-p-(\d+)', re.IGNORECASE)
def _check_ip_rate_limit(ip: str) -> bool:
now = time.time(); cutoff = now - 60.0
with _product_lookup_ip_lock:
ts = [t for t in _product_lookup_ip_cache.get(ip, []) if t > cutoff]
if len(ts) >= _PRODUCT_LOOKUP_MAX:
_product_lookup_ip_cache[ip] = ts; return False
ts.append(now); _product_lookup_ip_cache[ip] = ts; return True
# DISABLED: Questions, similar products, and followers features removed per user request
# questions_cache = {}
@@ -4128,6 +4143,39 @@ async def _stop_queue_worker():
_queue_worker = None
# ---------------------------------------------------------------------------
# Public product lookup endpoint (Ne Kadar Sattı? feature)
# ---------------------------------------------------------------------------
@app.get("/api/product/lookup", dependencies=[Depends(verify_api_key)])
def product_lookup(url: str, request: Request):
ip = request.headers.get("X-Forwarded-For", request.client.host or "unknown").split(",")[0].strip()
if not _check_ip_rate_limit(ip):
raise HTTPException(status_code=429, detail="Çok fazla istek. Bir dakika sonra tekrar deneyin.")
if not url or "trendyol.com" not in url.lower():
raise HTTPException(status_code=400, detail="Geçerli bir Trendyol URL'si girin.")
m = _TRENDYOL_URL_RE.search(url)
if not m:
raise HTTPException(status_code=400, detail="URL'den ürün ID'si alınamadı.")
product_id = int(m.group(1))
cached = product_lookup_cache.get(str(product_id))
if cached:
return {"source": "cache", "productId": product_id, **cached}
if _social_proof_breaker.is_open():
raise HTTPException(status_code=503, detail="Servis geçici olarak kullanılamıyor.")
data = fetch_social_proof([product_id])
if not data:
_social_proof_breaker.record_failure()
raise HTTPException(status_code=404, detail="Bu ürün için veri bulunamadı.")
items = data.get("result") or []
pd_item = next((it for it in items if it.get("contentId") == product_id), None)
if not pd_item:
raise HTTPException(status_code=404, detail="Sosyal kanıt verisi bulunamadı.")
result = {k: pd_item.get(k, 0) for k in ["orderCount", "favoriteCount", "pageViewCount", "basketCount"]}
product_lookup_cache.set(str(product_id), result)
_social_proof_breaker.record_success()
return {"source": "live", "productId": product_id, **result}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)