Changes
This commit is contained in:
16
.env
Normal file
16
.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# ── DigiKey ────────────────────────────────────────────────────────────────────
|
||||
# Register at https://developer.digikey.com/ → Create an organisation → Add a
|
||||
# production app → copy the Client ID and Client Secret below.
|
||||
DIGIKEY_CLIENT_ID=LnHz9vqQR4wSVglAAar6kkGseMagstWCVlF14F7t1CKZhnz5
|
||||
DIGIKEY_CLIENT_SECRET=93sQGOX3IXjlruhQfHDLBo0E98Wwyo63OCpwiZGKS3xZHkvmmeuQTJLtCnNMGjXM
|
||||
|
||||
# ── Mouser ─────────────────────────────────────────────────────────────────────
|
||||
# Register at https://www.mouser.com/api-hub/ → Apply for a Search API key.
|
||||
MOUSER_API_KEY=3afbcf04-b3fb-4d3d-8d0e-a21311b744f4
|
||||
|
||||
# ── Farnell / Element14 ────────────────────────────────────────────────────────
|
||||
# Register at https://partner.element14.com/ → My Account → Manage API Keys.
|
||||
# FARNELL_STORE controls which regional store is queried for pricing.
|
||||
# Examples: fr.farnell.com de.farnell.com uk.farnell.com ie.farnell.com
|
||||
FARNELL_API_KEY=3e4h2bnkdnwgzwcn6sqzvegf
|
||||
FARNELL_STORE=fr.farnell.com
|
||||
@@ -15,6 +15,3 @@ MOUSER_API_KEY=
|
||||
FARNELL_API_KEY=
|
||||
FARNELL_STORE=fr.farnell.com
|
||||
|
||||
# ── RS Components ──────────────────────────────────────────────────────────────
|
||||
# Register at https://developers.rs-online.com/ → Create an application.
|
||||
RS_API_KEY=
|
||||
|
||||
@@ -11,7 +11,6 @@ For every unique (Manufacturer, MPN) pair found, the script queries:
|
||||
- DigiKey (v4 API – developer.digikey.com)
|
||||
- Mouser (v1 API – mouser.com/api-hub)
|
||||
- Farnell (Element14 API – partner.element14.com)
|
||||
- RS Components (v2 API – developers.rs-online.com)
|
||||
|
||||
All prices are converted to EUR at quantity 1000 using live FX rates
|
||||
from Frankfurter (api.frankfurter.app – free, no key required).
|
||||
@@ -490,62 +489,6 @@ class FarnellClient:
|
||||
return best
|
||||
|
||||
|
||||
class RSClient:
|
||||
"""RS Components API v2. Register at developers.rs-online.com."""
|
||||
|
||||
_URL = "https://api.rs-online.com/searchProducts/v2/search"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.api_key = os.getenv("RS_API_KEY", "")
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.api_key)
|
||||
|
||||
def get_price_eur(self, manufacturer: str, mpn: str) -> Optional[float]:
|
||||
if not self.configured:
|
||||
return None
|
||||
try:
|
||||
r = requests.get(
|
||||
self._URL,
|
||||
params={
|
||||
"term": mpn,
|
||||
"fields": "prices,partNumber,manufacturer,manufacturerPartNumber",
|
||||
"size": 5,
|
||||
},
|
||||
headers={"apiKey": self.api_key},
|
||||
timeout=15,
|
||||
)
|
||||
r.raise_for_status()
|
||||
products = r.json().get("products", [])
|
||||
|
||||
for product in products:
|
||||
pn = product.get("manufacturerPartNumber", "")
|
||||
if str(pn).lower() == mpn.lower():
|
||||
p = self._price_at_qty(product)
|
||||
if p is not None:
|
||||
return p
|
||||
if products:
|
||||
return self._price_at_qty(products[0])
|
||||
except Exception as exc:
|
||||
log.debug(f"RS error [{mpn}]: {exc}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _price_at_qty(product: dict) -> Optional[float]:
|
||||
currency = product.get("priceCurrency", "EUR")
|
||||
best: Optional[float] = None
|
||||
for pb in product.get("prices", []):
|
||||
try:
|
||||
qty = int(pb.get("from") or pb.get("quantity") or 0)
|
||||
price = float(pb.get("price") or pb.get("unitPrice") or 0)
|
||||
if qty <= QUANTITY and price > 0:
|
||||
best = to_eur(price, currency)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return best
|
||||
|
||||
|
||||
# ── Result dataclass ───────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
@@ -555,17 +498,16 @@ class PartResult:
|
||||
digikey_eur: Optional[float] = None
|
||||
mouser_eur: Optional[float] = None
|
||||
farnell_eur: Optional[float] = None
|
||||
rs_eur: Optional[float] = None
|
||||
|
||||
@property
|
||||
def average_eur(self) -> Optional[float]:
|
||||
vals = [v for v in (self.digikey_eur, self.mouser_eur, self.farnell_eur, self.rs_eur)
|
||||
vals = [v for v in (self.digikey_eur, self.mouser_eur, self.farnell_eur)
|
||||
if v is not None]
|
||||
return round(sum(vals) / len(vals), 5) if vals else None
|
||||
|
||||
def sources_found(self) -> int:
|
||||
return sum(v is not None for v in
|
||||
(self.digikey_eur, self.mouser_eur, self.farnell_eur, self.rs_eur))
|
||||
(self.digikey_eur, self.mouser_eur, self.farnell_eur))
|
||||
|
||||
|
||||
# ── API lookup with caching ────────────────────────────────────────────────────
|
||||
@@ -577,7 +519,6 @@ def lookup(
|
||||
dk: DigiKeyClient,
|
||||
mu: MouserClient,
|
||||
fa: FarnellClient,
|
||||
rs: RSClient,
|
||||
) -> PartResult:
|
||||
cached = cache.get(manufacturer, mpn)
|
||||
if cached:
|
||||
@@ -588,20 +529,17 @@ def lookup(
|
||||
digikey_eur=cached.get("digikey_eur"),
|
||||
mouser_eur=cached.get("mouser_eur"),
|
||||
farnell_eur=cached.get("farnell_eur"),
|
||||
rs_eur=cached.get("rs_eur"),
|
||||
)
|
||||
|
||||
result = PartResult(manufacturer=manufacturer, mpn=mpn)
|
||||
result.digikey_eur = dk.get_price_eur(manufacturer, mpn); time.sleep(REQUEST_DELAY)
|
||||
result.mouser_eur = mu.get_price_eur(manufacturer, mpn); time.sleep(REQUEST_DELAY)
|
||||
result.farnell_eur = fa.get_price_eur(manufacturer, mpn); time.sleep(REQUEST_DELAY)
|
||||
result.rs_eur = rs.get_price_eur(manufacturer, mpn)
|
||||
result.farnell_eur = fa.get_price_eur(manufacturer, mpn)
|
||||
|
||||
cache.put(manufacturer, mpn, {
|
||||
"digikey_eur": result.digikey_eur,
|
||||
"mouser_eur": result.mouser_eur,
|
||||
"farnell_eur": result.farnell_eur,
|
||||
"rs_eur": result.rs_eur,
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -692,9 +630,8 @@ def main() -> None:
|
||||
dk = DigiKeyClient()
|
||||
mu = MouserClient()
|
||||
fa = FarnellClient()
|
||||
rs = RSClient()
|
||||
|
||||
active = [n for n, c in [("DigiKey", dk), ("Mouser", mu), ("Farnell", fa), ("RS", rs)]
|
||||
active = [n for n, c in [("DigiKey", dk), ("Mouser", mu), ("Farnell", fa)]
|
||||
if c.configured]
|
||||
if active:
|
||||
log.info(f"Configured APIs: {', '.join(active)}")
|
||||
@@ -713,9 +650,9 @@ def main() -> None:
|
||||
total = len(parts)
|
||||
for i, (mfr, mpn) in enumerate(sorted(parts), 1):
|
||||
log.info(f"[{i:>4}/{total}] {mfr or '(no mfr)':35s} {mpn}")
|
||||
r = lookup(mfr, mpn, cache, dk, mu, fa, rs)
|
||||
r = lookup(mfr, mpn, cache, dk, mu, fa)
|
||||
tag = f"€{r.average_eur:.4f}" if r.average_eur is not None else "—"
|
||||
log.info(f" avg {tag} ({r.sources_found()}/4 sources)")
|
||||
log.info(f" avg {tag} ({r.sources_found()}/3 sources)")
|
||||
price_lookup[_part_key(mfr, mpn)] = r.average_eur
|
||||
|
||||
write_back(file_map, price_lookup)
|
||||
|
||||
Reference in New Issue
Block a user