snipe/app/db/models.py
pyr0ball 6ec0f957b9 feat(snipe): auction support + easter eggs (Konami, The Steal, de-emphasis)
Auction metadata:
- Listing model gains buying_format + ends_at fields
- Migration 002 adds columns to existing databases
- scraper.py: parse s-item__time-left → absolute ends_at ISO timestamp
- normaliser.py: extract buyingOptions + itemEndDate from Browse API
- store.py: save/get updated for new fields

Easter eggs (app/ui/components/easter_eggs.py):
- Konami code detector (JS → URL param → Streamlit rerun)
- Web Audio API snipe call synthesis, gated behind sidebar checkbox
  (disabled by default for safety/accessibility)
- "The Steal" gold shimmer: trust ≥ 90, price 15–30% below market,
  no suspicious_price flag
- Auction de-emphasis: soft caption when > 1h remaining

UI updates:
- listing_row: steal banner + auction notice per row
- Search: inject CSS, check snipe mode, "Ending soon" sort option,
  pass market_price from comp cache to row renderer
- app.py: Konami detector + audio enable/disable sidebar toggle

Tests: 22 new tests (72 total, all green)
2026-03-25 14:27:02 -07:00

86 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Dataclasses for all Snipe domain objects."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Seller:
platform: str
platform_seller_id: str
username: str
account_age_days: int
feedback_count: int
feedback_ratio: float # 0.01.0
category_history_json: str # JSON blob of past category sales
id: Optional[int] = None
fetched_at: Optional[str] = None
@dataclass
class Listing:
platform: str
platform_listing_id: str
title: str
price: float
currency: str
condition: str
seller_platform_id: str
url: str
photo_urls: list[str] = field(default_factory=list)
listing_age_days: int = 0
buying_format: str = "fixed_price" # "fixed_price", "auction", "best_offer"
ends_at: Optional[str] = None # ISO8601 auction end time; None for fixed-price
id: Optional[int] = None
fetched_at: Optional[str] = None
trust_score_id: Optional[int] = None
@dataclass
class TrustScore:
listing_id: int
composite_score: int # 0100
account_age_score: int # 020
feedback_count_score: int # 020
feedback_ratio_score: int # 020
price_vs_market_score: int # 020
category_history_score: int # 020
photo_hash_duplicate: bool = False
photo_analysis_json: Optional[str] = None
red_flags_json: str = "[]"
score_is_partial: bool = False
id: Optional[int] = None
scored_at: Optional[str] = None
@dataclass
class MarketComp:
platform: str
query_hash: str
median_price: float
sample_count: int
expires_at: str # ISO8601 — checked against current time
id: Optional[int] = None
fetched_at: Optional[str] = None
@dataclass
class SavedSearch:
"""Schema scaffolded in v0.1; background monitoring wired in v0.2."""
name: str
query: str
platform: str
filters_json: str = "{}"
id: Optional[int] = None
created_at: Optional[str] = None
last_run_at: Optional[str] = None
@dataclass
class PhotoHash:
"""Perceptual hash store for cross-search dedup (v0.2+). Schema scaffolded in v0.1."""
listing_id: int
photo_url: str
phash: str # hex string from imagehash
id: Optional[int] = None
first_seen_at: Optional[str] = None