- ci.yml: API lint (ruff F+I) + pytest, web vue-tsc + vitest + build - mirror.yml: push to GitHub (CircuitForgeLLC) + Codeberg (CircuitForge) on main/tags - release.yml: Docker build → Forgejo registry + release via API; GHCR deferred pending BSL policy (cf-agents#3) - .cliff.toml: git-cliff changelog config for semver releases - pyproject.toml: add [dev] extras (pytest, ruff), ruff config - Fix 45 ruff violations across codebase (import sorting, unused vars, unused imports)
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""Convert raw eBay API responses into Snipe domain objects."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from app.db.models import Listing, Seller
|
|
|
|
|
|
def normalise_listing(raw: dict) -> Listing:
|
|
price_data = raw.get("price", {})
|
|
photos = []
|
|
if "image" in raw:
|
|
photos.append(raw["image"].get("imageUrl", ""))
|
|
for img in raw.get("additionalImages", []):
|
|
url = img.get("imageUrl", "")
|
|
if url and url not in photos:
|
|
photos.append(url)
|
|
photos = [p for p in photos if p]
|
|
|
|
listing_age_days = 0
|
|
created_raw = raw.get("itemCreationDate", "")
|
|
if created_raw:
|
|
try:
|
|
created = datetime.fromisoformat(created_raw.replace("Z", "+00:00"))
|
|
listing_age_days = (datetime.now(timezone.utc) - created).days
|
|
except ValueError:
|
|
pass
|
|
|
|
options = raw.get("buyingOptions", [])
|
|
if "AUCTION" in options:
|
|
buying_format = "auction"
|
|
elif "BEST_OFFER" in options:
|
|
buying_format = "best_offer"
|
|
else:
|
|
buying_format = "fixed_price"
|
|
|
|
ends_at = None
|
|
end_raw = raw.get("itemEndDate", "")
|
|
if end_raw:
|
|
try:
|
|
ends_at = datetime.fromisoformat(end_raw.replace("Z", "+00:00")).isoformat()
|
|
except ValueError:
|
|
pass
|
|
|
|
# Leaf category is categories[0] (most specific); parent path follows.
|
|
categories = raw.get("categories", [])
|
|
category_name: Optional[str] = categories[0]["categoryName"] if categories else None
|
|
|
|
seller = raw.get("seller", {})
|
|
return Listing(
|
|
platform="ebay",
|
|
platform_listing_id=raw["itemId"],
|
|
title=raw.get("title", ""),
|
|
price=float(price_data.get("value", 0)),
|
|
currency=price_data.get("currency", "USD"),
|
|
condition=raw.get("condition", "").lower(),
|
|
seller_platform_id=seller.get("username", ""),
|
|
url=raw.get("itemWebUrl", ""),
|
|
photo_urls=photos,
|
|
listing_age_days=listing_age_days,
|
|
buying_format=buying_format,
|
|
ends_at=ends_at,
|
|
category_name=category_name,
|
|
)
|
|
|
|
|
|
def normalise_seller(raw: dict) -> Seller:
|
|
feedback_pct = float(raw.get("feedbackPercentage", "0").strip("%")) / 100.0
|
|
|
|
account_age_days: Optional[int] = None # None = registrationDate not in API response
|
|
reg_date_raw = raw.get("registrationDate", "")
|
|
if reg_date_raw:
|
|
try:
|
|
reg_date = datetime.fromisoformat(reg_date_raw.replace("Z", "+00:00"))
|
|
account_age_days = (datetime.now(timezone.utc) - reg_date).days
|
|
except ValueError:
|
|
pass
|
|
|
|
category_history = {}
|
|
summary = raw.get("sellerFeedbackSummary", {})
|
|
for entry in summary.get("feedbackByCategory", []):
|
|
category_history[entry.get("categorySite", "")] = int(entry.get("count", 0))
|
|
|
|
return Seller(
|
|
platform="ebay",
|
|
platform_seller_id=raw["username"],
|
|
username=raw["username"],
|
|
account_age_days=account_age_days,
|
|
feedback_count=int(raw.get("feedbackScore", 0)),
|
|
feedback_ratio=feedback_pct,
|
|
category_history_json=json.dumps(category_history),
|
|
)
|