diff --git a/.env b/.env new file mode 100644 index 0000000..0f23189 --- /dev/null +++ b/.env @@ -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 diff --git a/.env.example b/.env.example index fb114e5..7d63e72 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/bom_price_checker.py b/bom_price_checker.py index 1b54f84..01c98a1 100644 --- a/bom_price_checker.py +++ b/bom_price_checker.py @@ -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)