Core trust scoring: - Five metadata signals (account age, feedback count/ratio, price vs market, category history), composited 0–100 - CV-based price signal suppression for heterogeneous search results (e.g. mixed laptop generations won't false-positive suspicious_price) - Expanded scratch/dent title detection: evasive redirects, functional problem phrases, DIY/repair indicators - Hard filters: new_account, established_bad_actor - Soft flags: low_feedback, suspicious_price, duplicate_photo, scratch_dent, long_on_market, significant_price_drop Search & filtering: - Browse API adapter (up to 200 items/page) + Playwright scraper fallback - OR-group query expansion for comprehensive variant coverage - Must-include (AND/ANY/groups), must-exclude, category, price range filters - Saved searches with full filter round-trip via URL params Seller enrichment: - Background BTF /itm/ scraping for account age (Kasada-safe headed Chromium) - On-demand enrichment: POST /api/enrich + ListingCard ↻ button - Category history derived from Browse API categories field (free, no extra calls) - Shopping API GetUserProfile inline enrichment for API adapter Market comps: - eBay Marketplace Insights API with Browse API fallback (catches 403 + 404) - Comps prioritised in ThreadPoolExecutor (submitted first) Infrastructure: - Staging DB fields: times_seen, first_seen_at, price_at_first_seen, category_name - Migrations 004 (staging tracking) + 005 (listing category) - eBay webhook handler stub - Cloud compose stack (compose.cloud.yml) - Vue frontend: search store, saved searches store, ListingCard, filter sidebar Docs: - README fully rewritten to reflect MVP status + full feature documentation - Roadmap table linked to all 13 Forgejo issues
50 lines
1.7 KiB
Python
50 lines
1.7 KiB
Python
"""eBay OAuth2 client credentials token manager."""
|
|
from __future__ import annotations
|
|
import base64
|
|
import time
|
|
from typing import Optional
|
|
import requests
|
|
|
|
EBAY_OAUTH_URLS = {
|
|
"production": "https://api.ebay.com/identity/v1/oauth2/token",
|
|
"sandbox": "https://api.sandbox.ebay.com/identity/v1/oauth2/token",
|
|
}
|
|
|
|
|
|
class EbayTokenManager:
|
|
"""Fetches and caches eBay app-level OAuth tokens. Thread-safe for single process."""
|
|
|
|
def __init__(self, client_id: str, client_secret: str, env: str = "production"):
|
|
self._client_id = client_id
|
|
self._client_secret = client_secret
|
|
self._token_url = EBAY_OAUTH_URLS[env]
|
|
self._token: Optional[str] = None
|
|
self._expires_at: float = 0.0
|
|
|
|
@property
|
|
def client_id(self) -> str:
|
|
return self._client_id
|
|
|
|
def get_token(self) -> str:
|
|
"""Return a valid access token, fetching or refreshing as needed."""
|
|
if self._token and time.time() < self._expires_at - 60:
|
|
return self._token
|
|
self._fetch_token()
|
|
return self._token # type: ignore[return-value]
|
|
|
|
def _fetch_token(self) -> None:
|
|
credentials = base64.b64encode(
|
|
f"{self._client_id}:{self._client_secret}".encode()
|
|
).decode()
|
|
resp = requests.post(
|
|
self._token_url,
|
|
headers={
|
|
"Authorization": f"Basic {credentials}",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
data={"grant_type": "client_credentials", "scope": "https://api.ebay.com/oauth/api_scope"},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
self._token = data["access_token"]
|
|
self._expires_at = time.time() + data["expires_in"]
|