Shoaib898 commited on
Commit
aa120d2
Β·
verified Β·
1 Parent(s): 900c867

Deploy Rankora API (clean bundle)

Browse files
app/routers/products.py CHANGED
@@ -127,7 +127,7 @@ def get_product(
127
  @router.get("/{asin}/buy-box/history")
128
  def get_buy_box_history(
129
  asin: str,
130
- days: int = Query(90, ge=7, le=365, description="30 / 90 / 180 / 365 β€” Keepa backfill when key set"),
131
  db: Session = Depends(get_db),
132
  current_user: User = Depends(get_current_user),
133
  ):
@@ -136,7 +136,7 @@ def get_buy_box_history(
136
  if not product:
137
  raise HTTPException(status_code=404, detail="Product not found")
138
 
139
- # Enough Rankora price points for yearly charts (Keepa fills gaps)
140
  hist_limit = 2000 if days >= 180 else 500
141
  latest = (
142
  db.query(PriceHistory)
 
127
  @router.get("/{asin}/buy-box/history")
128
  def get_buy_box_history(
129
  asin: str,
130
+ days: int = Query(90, ge=7, le=365, description="30 / 90 / 180 / 365 β€” Rankora scrape history"),
131
  db: Session = Depends(get_db),
132
  current_user: User = Depends(get_current_user),
133
  ):
 
136
  if not product:
137
  raise HTTPException(status_code=404, detail="Product not found")
138
 
139
+ # Enough Rankora price points for long-range charts
140
  hist_limit = 2000 if days >= 180 else 500
141
  latest = (
142
  db.query(PriceHistory)
app/services/analytics/buy_box_history.py CHANGED
@@ -1,8 +1,7 @@
1
  """Persist and analyze Buy Box winner history over time.
2
 
3
- Sources:
4
- - Rankora `buy_box_snapshots` (live scrapes going forward)
5
- - Keepa Product API (optional backfill for 6–12 months when KEEPA_API_KEY is set)
6
  """
7
  from __future__ import annotations
8
 
@@ -151,10 +150,10 @@ def merge_snapshots(
151
  keepa_dicts: Optional[List[dict]] = None,
152
  range_days: int = 365,
153
  ) -> List[SnapshotLike]:
154
- """Merge Keepa backfill + Rankora live snapshots.
155
 
156
- Rule: Rankora wins for any day it has data (more accurate live scrape).
157
- Keepa fills older gaps for 6–12 month charts.
158
  """
159
  cutoff = datetime.now(timezone.utc) - timedelta(days=max(range_days, 1))
160
  local = [orm_to_snapshot_like(s) for s in rankora]
@@ -283,7 +282,7 @@ def rotation_from_snapshots(
283
  "total_sellers": 0,
284
  "source": "insufficient_data",
285
  "snapshot_count": len(snapshots),
286
- "disclaimer": "Refresh this product a few times to build Buy Box history, or set KEEPA_API_KEY for backfill.",
287
  }
288
 
289
  now = datetime.now(timezone.utc)
@@ -373,8 +372,9 @@ def rotation_from_snapshots(
373
  "source": hist_source,
374
  "snapshot_count": len(snapshots),
375
  "disclaimer": (
376
- f"Buy Box share from {len(snapshots)} snapshots over {range_days} days "
377
- f"(source: {hist_source}). Keepa fills past months; Rankora scrapes keep live data accurate."
 
378
  ),
379
  }
380
 
@@ -524,16 +524,34 @@ def build_market_history_summary(
524
  bb_prices = [float(s.price) for s in snapshots if s.price is not None]
525
  hist_prices = [float(r["price"]) for r in price_history if r.get("price") is not None]
526
  all_prices = bb_prices or hist_prices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  return {
528
  "range_days": range_days,
529
  "buy_box_snapshots": len(snapshots),
530
  "price_points": len(price_history),
 
 
531
  "price_current": round(all_prices[-1], 2) if all_prices else None,
532
  "price_avg": round(sum(all_prices) / len(all_prices), 2) if all_prices else None,
533
  "price_min": round(min(all_prices), 2) if all_prices else None,
534
  "price_max": round(max(all_prices), 2) if all_prices else None,
535
- "first_snapshot": _ensure_utc(snapshots[0].recorded_at).isoformat() if snapshots else None,
536
- "last_snapshot": _ensure_utc(snapshots[-1].recorded_at).isoformat() if snapshots else None,
537
  "first_price_record": price_history[0]["recorded_at"] if price_history else None,
538
  "last_price_record": price_history[-1]["recorded_at"] if price_history else None,
539
  "buy_box_price_min": round(min(bb_prices), 2) if bb_prices else None,
@@ -693,11 +711,11 @@ def build_historical_charts_payload(
693
  "bsr_yearly": build_yearly_aggregates(price_history, "bsr"),
694
  "range_options": [30, 90, 180, 365],
695
  "how_it_works": [
696
- "Rankora scrapes Amazon every ~6 hours and saves Buy Box winner + price (live forward history).",
697
- "Keepa API (optional) backfills 6–12 months of price + Buy Box ownership before you tracked the ASIN.",
698
- "Charts merge both: Keepa for the past, Rankora for recent/live accuracy.",
699
- "Buy Box % = hours each seller held the box between snapshots (time-weighted).",
700
- "Set KEEPA_API_KEY in Hugging Face / .env secrets to enable multi-month backfill.",
701
  ],
702
  }
703
 
 
1
  """Persist and analyze Buy Box winner history over time.
2
 
3
+ Primary source: Rankora `buy_box_snapshots` + `price_history` (forward scrapes).
4
+ Keepa Product API remains optional/paid and dormant unless KEEPA_API_KEY is set.
 
5
  """
6
  from __future__ import annotations
7
 
 
150
  keepa_dicts: Optional[List[dict]] = None,
151
  range_days: int = 365,
152
  ) -> List[SnapshotLike]:
153
+ """Merge optional Keepa rows + Rankora live snapshots.
154
 
155
+ Default FYP path: keepa_dicts empty β†’ Rankora-only.
156
+ If Keepa is ever enabled: Rankora wins on overlapping days.
157
  """
158
  cutoff = datetime.now(timezone.utc) - timedelta(days=max(range_days, 1))
159
  local = [orm_to_snapshot_like(s) for s in rankora]
 
282
  "total_sellers": 0,
283
  "source": "insufficient_data",
284
  "snapshot_count": len(snapshots),
285
+ "disclaimer": "Refresh this product a few times (or wait for the 6h tracker) to build Buy Box history from Rankora scrapes.",
286
  }
287
 
288
  now = datetime.now(timezone.utc)
 
372
  "source": hist_source,
373
  "snapshot_count": len(snapshots),
374
  "disclaimer": (
375
+ f"Buy Box share from {len(snapshots)} Rankora snapshots over {range_days} days "
376
+ f"(source: {hist_source}). History starts when Rankora first tracked this ASIN β€” "
377
+ "keep Refreshing / tracking for fuller 180d–365d charts."
378
  ),
379
  }
380
 
 
524
  bb_prices = [float(s.price) for s in snapshots if s.price is not None]
525
  hist_prices = [float(r["price"]) for r in price_history if r.get("price") is not None]
526
  all_prices = bb_prices or hist_prices
527
+ first_snap = _ensure_utc(snapshots[0].recorded_at) if snapshots else None
528
+ last_snap = _ensure_utc(snapshots[-1].recorded_at) if snapshots else None
529
+ span_days = 0
530
+ if first_snap and last_snap:
531
+ span_days = max(1, (last_snap - first_snap).days + 1)
532
+ elif price_history:
533
+ try:
534
+ first_p = datetime.fromisoformat(str(price_history[0]["recorded_at"]).replace("Z", "+00:00"))
535
+ last_p = datetime.fromisoformat(str(price_history[-1]["recorded_at"]).replace("Z", "+00:00"))
536
+ if first_p.tzinfo is None:
537
+ first_p = first_p.replace(tzinfo=timezone.utc)
538
+ if last_p.tzinfo is None:
539
+ last_p = last_p.replace(tzinfo=timezone.utc)
540
+ span_days = max(1, (last_p - first_p).days + 1)
541
+ except Exception:
542
+ span_days = 0
543
  return {
544
  "range_days": range_days,
545
  "buy_box_snapshots": len(snapshots),
546
  "price_points": len(price_history),
547
+ "history_span_days": span_days,
548
+ "partial_range": bool(span_days and span_days < range_days),
549
  "price_current": round(all_prices[-1], 2) if all_prices else None,
550
  "price_avg": round(sum(all_prices) / len(all_prices), 2) if all_prices else None,
551
  "price_min": round(min(all_prices), 2) if all_prices else None,
552
  "price_max": round(max(all_prices), 2) if all_prices else None,
553
+ "first_snapshot": first_snap.isoformat() if first_snap else None,
554
+ "last_snapshot": last_snap.isoformat() if last_snap else None,
555
  "first_price_record": price_history[0]["recorded_at"] if price_history else None,
556
  "last_price_record": price_history[-1]["recorded_at"] if price_history else None,
557
  "buy_box_price_min": round(min(bb_prices), 2) if bb_prices else None,
 
711
  "bsr_yearly": build_yearly_aggregates(price_history, "bsr"),
712
  "range_options": [30, 90, 180, 365],
713
  "how_it_works": [
714
+ "Rankora scrapes Amazon every ~6 hours (or on Refresh) and saves Buy Box winner + price.",
715
+ "History is forward-only: the timeline starts when Rankora first tracked this ASIN.",
716
+ "30d / 90d charts fill quickly; 180d / 365d grow as you keep the ASIN tracked.",
717
+ "Buy Box % = hours each seller held the box between Rankora snapshots (time-weighted).",
718
+ "No paid history API is required β€” Keepa remains optional/paid and unused when unset.",
719
  ],
720
  }
721
 
app/services/external/__init__.py CHANGED
@@ -1 +1 @@
1
- """Third-party data providers (Keepa, etc.)."""
 
1
+ """Optional third-party providers (Keepa is paid-only; Rankora scrapes are default)."""
app/services/external/keepa_client.py CHANGED
@@ -1,9 +1,9 @@
1
- """Keepa Product API β€” historical price + Buy Box ownership (US marketplace).
 
 
 
2
 
3
  Docs: https://keepa.com/#!discuss/t/product-object/116
4
- - Keepa time = minutes since 2011-01-01 UTC
5
- - Prices in cents (βˆ’1 = out of stock / no offer)
6
- - buybox=1 adds BUY_BOX_SHIPPING csv + buyBoxSellerIdHistory
7
  """
8
  from __future__ import annotations
9
 
 
1
+ """Keepa Product API β€” optional PAID backfill (dormant without KEEPA_API_KEY).
2
+
3
+ Rankora FYP default is Rankora-only scrape history. Keepa has no free API tier.
4
+ This module is unused unless a paid key is configured.
5
 
6
  Docs: https://keepa.com/#!discuss/t/product-object/116
 
 
 
7
  """
8
  from __future__ import annotations
9
 
app/services/product_service.py CHANGED
@@ -378,7 +378,7 @@ def build_response(db: Session, product: Product, data_source: str = "cached") -
378
  reviews = latest.review_count if latest else 0
379
 
380
  bb_data = _buy_box_data_from_product(product)
381
- # Local Rankora history for product page (Keepa backfill loads on History tab via /buy-box/history)
382
  snapshots = get_buy_box_snapshots(db, str(product.id), days=180, limit=2000)
383
  rotation = rotation_from_snapshots(snapshots, range_days=90, fallback_data={**bb_data, "price": price})
384
  history_timeline = build_buy_box_timeline(snapshots, range_days=90)
 
378
  reviews = latest.review_count if latest else 0
379
 
380
  bb_data = _buy_box_data_from_product(product)
381
+ # Rankora-only history for product page (longer ranges via /buy-box/history)
382
  snapshots = get_buy_box_snapshots(db, str(product.id), days=180, limit=2000)
383
  rotation = rotation_from_snapshots(snapshots, range_days=90, fallback_data={**bb_data, "price": price})
384
  history_timeline = build_buy_box_timeline(snapshots, range_days=90)