snipe/app/trust/__init__.py
pyr0ball eb05be0612
Some checks are pending
CI / API — lint + test (pull_request) Waiting to run
CI / Web — typecheck + test + build (pull_request) Waiting to run
feat: wire Forgejo Actions CI/CD workflows (#22)
- 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)
2026-04-06 00:00:28 -07:00

66 lines
2.5 KiB
Python

import hashlib
import math
from app.db.models import Listing, TrustScore
from app.db.models import Seller as Seller
from app.db.store import Store
from .aggregator import Aggregator
from .metadata import MetadataScorer
from .photo import PhotoScorer
class TrustScorer:
"""Orchestrates metadata + photo scoring for a batch of listings."""
def __init__(self, shared_store: Store):
self._store = shared_store
self._meta = MetadataScorer()
self._photo = PhotoScorer()
self._agg = Aggregator()
def score_batch(
self,
listings: list[Listing],
query: str,
) -> list[TrustScore]:
query_hash = hashlib.md5(query.encode()).hexdigest()
comp = self._store.get_market_comp("ebay", query_hash)
market_median = comp.median_price if comp else None
# Coefficient of variation: stddev/mean across batch prices.
# None when fewer than 2 priced listings (can't compute variance).
_prices = [l.price for l in listings if l.price > 0]
if len(_prices) >= 2:
_mean = sum(_prices) / len(_prices)
_stddev = math.sqrt(sum((p - _mean) ** 2 for p in _prices) / len(_prices))
price_cv: float | None = _stddev / _mean if _mean > 0 else None
else:
price_cv = None
photo_url_sets = [l.photo_urls for l in listings]
duplicates = self._photo.check_duplicates(photo_url_sets)
scores = []
for listing, is_dup in zip(listings, duplicates):
seller = self._store.get_seller("ebay", listing.seller_platform_id)
blocklisted = self._store.is_blocklisted("ebay", listing.seller_platform_id)
if seller:
signal_scores = self._meta.score(seller, market_median, listing.price, price_cv)
else:
signal_scores = {k: None for k in
["account_age", "feedback_count", "feedback_ratio",
"price_vs_market", "category_history"]}
trust = self._agg.aggregate(
signal_scores, is_dup, seller,
listing_id=listing.id or 0,
listing_title=listing.title,
listing_condition=listing.condition,
times_seen=listing.times_seen,
first_seen_at=listing.first_seen_at,
price=listing.price,
price_at_first_seen=listing.price_at_first_seen,
is_blocklisted=blocklisted,
)
scores.append(trust)
return scores