import os import time from typing import Tuple import httpx from fastapi import APIRouter, HTTPException, Query router = APIRouter(tags=["octopart"]) NEXAR_TOKEN_URL = "https://identity.nexar.com/connect/token" NEXAR_API_URL = "https://api.nexar.com/graphql" _token_cache: dict = {"token": None, "expires_at": 0.0} _SEARCH_GQL = """ query PartSearch($q: String!, $limit: Int!) { supSearchMpn(q: $q, limit: $limit) { results { part { mpn manufacturer { name } shortDescription bestDatasheet { url } } } } } """ def _credentials() -> Tuple[str, str]: return ( os.getenv("NEXAR_CLIENT_ID", ""), os.getenv("NEXAR_CLIENT_SECRET", ""), ) async def _get_token() -> str: client_id, client_secret = _credentials() if not client_id or not client_secret: raise HTTPException( status_code=503, detail="Nexar/Octopart credentials not configured — set NEXAR_CLIENT_ID and NEXAR_CLIENT_SECRET environment variables", ) now = time.time() if _token_cache["token"] and now < _token_cache["expires_at"] - 60: return _token_cache["token"] # type: ignore[return-value] async with httpx.AsyncClient() as client: resp = await client.post( NEXAR_TOKEN_URL, data={ "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, }, timeout=10, ) if resp.status_code != 200: raise HTTPException(status_code=502, detail=f"Nexar auth failed: {resp.text[:200]}") body = resp.json() _token_cache["token"] = body["access_token"] _token_cache["expires_at"] = now + body.get("expires_in", 3600) return _token_cache["token"] # type: ignore[return-value] @router.get("/status") async def octopart_status(): client_id, client_secret = _credentials() return {"configured": bool(client_id and client_secret)} @router.get("/search") async def octopart_search( q: str = Query(..., min_length=1), limit: int = Query(8, ge=1, le=20), ): token = await _get_token() async with httpx.AsyncClient() as client: resp = await client.post( NEXAR_API_URL, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, json={"query": _SEARCH_GQL, "variables": {"q": q, "limit": limit}}, timeout=12, ) if resp.status_code != 200: raise HTTPException(status_code=502, detail=f"Nexar API error {resp.status_code}") body = resp.json() if "errors" in body: raise HTTPException(status_code=502, detail=str(body["errors"])[:300]) results = body.get("data", {}).get("supSearchMpn", {}).get("results", []) out = [] for r in results: part = r.get("part") if not part: continue out.append({ "mpn": part.get("mpn", ""), "manufacturer": (part.get("manufacturer") or {}).get("name", ""), "description": part.get("shortDescription") or "", "datasheet_url": (part.get("bestDatasheet") or {}).get("url"), }) return out