DigiKey Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for datasheet sync | | | BOM management — orchestrates sourcing across distributors | | | Uses DigiKey parametric data for behavioral SPICE models | DigiKey is the primary source for prototype orders (Mouser is secondary). Its API returns direct PDF datasheet links, making it the preferred datasheet source. For production orders, see / . For BOM management and export workflows, see . API Credential Setup The DigiKey API requires OAuth 2.0 credentials. Here's how to set…

| xargs)\n ```\n\nThe client credentials flow has no user interaction — once configured, API calls work automatically.\n\n## DigiKey Product Information API v4\n\nThe API is the preferred way to search DigiKey. It returns structured JSON with full product details, pricing, stock, datasheets, and parametric data.\n\n**Base URL:** `https://api.digikey.com`\n\n### Authentication\n\nAll API requests require OAuth 2.0. Use the **client credentials flow** (2-legged). Credentials must be loaded as environment variables (see \"API Credential Setup\" above).\n\n```bash\ncurl -s -X POST https://api.digikey.com/v1/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"client_id=${DIGIKEY_CLIENT_ID}&client_secret=${DIGIKEY_CLIENT_SECRET}&grant_type=client_credentials\"\n```\n\nThe response returns an `access_token` valid for **10 minutes**. Cache the token in a shell variable and reuse it for subsequent calls in the same session. If you get a 401 error mid-session, the token has expired — re-authenticate to get a fresh one.\n\n### Required Headers\n\nEvery API call needs:\n```\nX-DIGIKEY-Client-Id: ${DIGIKEY_CLIENT_ID}\nAuthorization: Bearer \u003caccess_token>\n```\n\nOptional locale headers:\n- `X-DIGIKEY-Locale-Language`: `en` (default), `ja`, `de`, `fr`, `ko`, `zhs`, `zht`, `it`, `es`\n- `X-DIGIKEY-Locale-Currency`: `USD` (default), `CAD`, `EUR`, `GBP`, `JPY`, etc.\n- `X-DIGIKEY-Locale-Site`: `US` (default), `CA`, `UK`, `DE`, etc.\n\n### KeywordSearch — Find Parts\n\n```\nPOST /products/v4/search/keyword\n```\n\nThis is the primary search endpoint. Search by MPN, DigiKey part number, description, or keywords.\n\nRequest body:\n```json\n{\n \"Keywords\": \"GRM155R71C104KA88D\",\n \"Limit\": 25,\n \"Offset\": 0,\n \"FilterOptionsRequest\": {\n \"MinimumQuantityAvailable\": 1,\n \"SearchOptions\": [\"InStock\", \"HasDatasheet\", \"RoHSCompliant\"],\n \"ManufacturerFilter\": [{\"Id\": \"...\"}],\n \"CategoryFilter\": [{\"Id\": \"...\"}],\n \"StatusFilter\": [{\"Id\": \"...\"}],\n \"MarketPlaceFilter\": \"ExcludeMarketPlace\"\n },\n \"SortOptions\": {\n \"Field\": \"Price\",\n \"SortOrder\": \"Ascending\"\n }\n}\n```\n\nKey request fields:\n- `Keywords` (string, max 250 chars) — search term (MPN, DK PN, description)\n- `Limit` (int, 1-50) — results per page\n- `Offset` (int) — pagination offset\n- `SearchOptions` — array of: `InStock`, `HasDatasheet`, `RoHSCompliant`, `NormallyStocking`, `Has3DModel`, `HasCadModel`, `HasProductPhoto`, `NewProduct`\n- `SortOptions.Field` — `Price`, `QuantityAvailable`, `Manufacturer`, `ManufacturerProductNumber`, `DigiKeyProductNumber`, `MinimumQuantity`\n- `MarketPlaceFilter` — `NoFilter`, `ExcludeMarketPlace`, `MarketPlaceOnly`\n\nResponse — key fields in each `Products[]` item:\n```json\n{\n \"ManufacturerProductNumber\": \"GRM155R71C104KA88D\",\n \"Manufacturer\": {\"Id\": 563, \"Name\": \"Murata Electronics\"},\n \"Description\": {\n \"ProductDescription\": \"CAP CER 100NF 16V X7R 0402\",\n \"DetailedDescription\": \"...\"\n },\n \"UnitPrice\": 0.01,\n \"QuantityAvailable\": 248000,\n \"ProductUrl\": \"https://www.digikey.com/...\",\n \"DatasheetUrl\": \"https://...\",\n \"PhotoUrl\": \"https://...\",\n \"ProductVariations\": [\n {\n \"DigiKeyProductNumber\": \"490-10698-1-ND\",\n \"PackageType\": {\"Name\": \"Cut Tape\"},\n \"StandardPricing\": [\n {\"BreakQuantity\": 1, \"UnitPrice\": 0.01, \"TotalPrice\": 0.01},\n {\"BreakQuantity\": 10, \"UnitPrice\": 0.008, \"TotalPrice\": 0.08}\n ],\n \"QuantityAvailableforPackageType\": 248000,\n \"MinimumOrderQuantity\": 1,\n \"StandardPackage\": 10000\n }\n ],\n \"Parameters\": [\n {\"ParameterText\": \"Capacitance\", \"ValueText\": \"100nF\"},\n {\"ParameterText\": \"Voltage Rated\", \"ValueText\": \"16V\"},\n {\"ParameterText\": \"Temperature Coefficient\", \"ValueText\": \"X7R\"},\n {\"ParameterText\": \"Package / Case\", \"ValueText\": \"0402 (1005 Metric)\"}\n ],\n \"ProductStatus\": {\"Status\": \"Active\"},\n \"Category\": {\"Name\": \"Ceramic Capacitors\"},\n \"Classifications\": {\"RohsStatus\": \"ROHS3 Compliant\"},\n \"Discontinued\": false,\n \"EndOfLife\": false,\n \"NormallyStocking\": true\n}\n```\n\n### ProductDetails — Full Details for One Part\n\n```\nGET /products/v4/search/{productNumber}/productdetails\n```\n\nUse this for expanded information on a specific part. `{productNumber}` can be a DigiKey part number or manufacturer part number.\n\nQuery parameters:\n- `manufacturerId` (optional) — disambiguate MPNs that match multiple manufacturers (e.g., \"CR2032\")\n\nReturns the full `Product` object with all parameters, pricing (including MyPricing if authenticated with account), media links, and related products.\n\n### Other Useful Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/products/v4/search/{pn}/productdetails` | GET | Full product info for one part |\n| `/products/v4/search/productpricing/{pn}` | GET | Pricing with MyPricing for a part |\n| `/products/v4/search/{pn}/media` | GET | All media (images, datasheets) for a part |\n| `/products/v4/search/manufacturers` | GET | All manufacturers (use IDs in KeywordSearch filters) |\n| `/products/v4/search/categories` | GET | All categories (use IDs in KeywordSearch filters) |\n| `/products/v4/search/{pn}/alternatepackaging` | GET | Alternate packaging options |\n| `/products/v4/search/{pn}/substitutions` | GET | Substitute parts |\n| `/products/v4/search/{pn}/recommendedproducts` | GET | Recommended/associated parts |\n\n### Rate Limits\n\nPer-minute and daily quotas apply. HTTP 429 with `Retry-After` header on exceed.\n\n### Error Responses\n\nAll errors return `DKProblemDetails`:\n```json\n{\"type\": \"...\", \"title\": \"...\", \"status\": 401, \"detail\": \"Invalid token\", \"correlationId\": \"...\"}\n```\n\n## Fallback: Fetch DigiKey Website\n\nIf API credentials are not available or authentication fails, search DigiKey by fetching product pages directly:\n\n```\nhttps://www.digikey.com/en/products/result?keywords=\u003curl-encoded-query>\n```\n\nExamples:\n- `https://www.digikey.com/en/products/result?keywords=GRM155R71C104KA88D` (by MPN)\n- `https://www.digikey.com/en/products/result?keywords=100nF+0402+X7R+16V` (by specs)\n\nResults from DigiKey can be noisy (JS-heavy pages). Look for the product table rows containing: DigiKey part number, MPN, description, unit price, stock quantity, and datasheet links. If results are truncated or empty, try searching by exact MPN rather than keywords.\n\n## Datasheet Download & Analysis\n\nDigiKey's API provides **direct PDF URLs** for datasheets — this is the preferred method for downloading datasheets because it avoids web scraping and returns reliable, stable links. Other skills (kicad, bom) should use DigiKey API as the first-choice datasheet source.\n\n### Datasheet Directory Sync (Primary Workflow)\n\nUse `sync_datasheets_digikey.py` to maintain a `datasheets/` directory alongside a KiCad project. It extracts components from the schematic, searches DigiKey for datasheet URLs, downloads missing PDFs, and writes an `manifest.json` manifest. Subsequent runs are incremental — only new or changed parts are fetched.\n\n```bash\n# Sync datasheets for a KiCad project (creates datasheets/ next to the schematic)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch>\n\n# Preview what would be downloaded\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --dry-run\n\n# Retry previously failed downloads\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --force\n\n# Custom output directory\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> -o ./my-datasheets\n\n# Use pre-computed analyzer JSON instead of running the analyzer\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py analyzer_output.json\n\n# Parallel downloads (3 workers)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --parallel 3\n\n# Batch mode — sync from a plain MPN list (no KiCad project required)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py --mpn-list mpns.txt --output ./datasheets\n```\n\n**MPN-list batch mode** (KH-312) — when you have a list of MPNs but no KiCad\nproject to point at (harness datasheet seeding, bulk seeding a new part\nlibrary). The file format is one MPN per line. Blank lines and `#`\ncomments (full-line and inline) are skipped. Non-MPN strings (generic\nvalues like `100nF` or `DNP`) are filtered via `is_real_mpn()` and\nde-duplicated. Output defaults to `./datasheets/` in the current working\ndirectory when `--output` is omitted.\n\nThe script:\n- **Runs the kicad schematic analyzer** automatically to extract components and MPNs\n- **Filters generic passives** — skips entries without real MPNs (e.g., \"100nF\", \"10K\")\n- **Tries schematic URLs first** — uses the datasheet URL embedded in the KiCad symbol before hitting the DigiKey API, saving API calls\n- **Writes `manifest.json` manifest** — maps each MPN to its PDF file, manufacturer, description, download status, and URL. The kicad skill reads this during design review to cross-reference datasheets with the schematic.\n- **Tracks failures** — failed downloads are recorded with error details and not retried on subsequent runs unless `--force` is used\n- **Rate-limited** — 1 second between DigiKey API calls (configurable with `--delay`)\n- **Saves progress incrementally** — if interrupted, already-downloaded files are preserved\n\nThe `manifest.json` manifest structure:\n```json\n{\n \"schematic\": \"/path/to/file.kicad_sch\",\n \"last_sync\": \"2026-03-09T04:44:30+00:00\",\n \"parts\": {\n \"TPS61023DRLR\": {\n \"file\": \"TPS61023DRLR.pdf\",\n \"manufacturer\": \"Texas Instruments\",\n \"description\": \"Boost converter\",\n \"datasheet_url\": \"https://...\",\n \"status\": \"ok\",\n \"references\": [\"U3\", \"U2\"],\n \"size_bytes\": 2392725\n }\n }\n}\n```\n\n### Single Datasheet Download\n\nUse `fetch_datasheet_digikey.py` for one-off datasheet downloads. It handles manufacturer-specific quirks automatically.\n\n```bash\n# Search by MPN (uses DigiKey API, requires credentials)\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py --search \"TPS61023\" -o datasheet.pdf\n\n# Direct URL download\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py \"https://www.ti.com/lit/gpn/tps61023\" -o datasheet.pdf\n\n# JSON output for script integration\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py --search \"ADP1706\" --json\n```\n\nThe script:\n- **OS-agnostic** — uses Python `requests` library (no wget/curl dependency). Falls back to `urllib` if `requests` isn't installed.\n- **Normalizes redirect URLs** — DigiKey's `DatasheetUrl` for TI parts points to a JS redirect page; the script extracts the direct PDF link. Also fixes protocol-relative `//mm.digikey.com/...` URLs.\n- **Sets proper User-Agent** — many manufacturer sites (Nexperia, Lite-On, STMicro, Molex) block bare `urllib` or `curl` requests but serve PDFs fine with a browser User-Agent\n- **Validates PDF headers** — rejects HTML error pages or Cloudflare challenge pages that masquerade as downloads\n- **Falls back to alternative sources** — tries known URL patterns for Microchip when the primary URL fails\n- **Headless browser fallback** — if `playwright` is installed, automatically uses a headless Chromium browser as a last resort for sites that serve PDFs via JavaScript (Broadcom doc viewer, Espressif download redirects). Intercepts download events and reads response bodies directly.\n- **Exit codes**: 0 = success, 1 = download failed, 2 = search/API error\n- **Dependencies**:\n - `pip install requests` (strongly recommended; urllib fallback can't handle HTTP/2 sites like analog.com)\n - `pip install playwright && playwright install chromium` (optional; enables headless browser fallback for JS-heavy sites)\n\n### Manufacturer Compatibility\n\nTested against 240 components across 8 open-source KiCad projects (96% download success rate, 94% without Playwright):\n\n| Manufacturer | Status | Notes |\n|---|---|---|\n| TI | Works | URL normalization strips JS redirect wrapper |\n| ADI / Analog | Works | `requests` handles HTTP/2 transparently |\n| STMicro | Works | Requires User-Agent header |\n| Nexperia | Works | Requires User-Agent header |\n| Lite-On | Works | Requires User-Agent header |\n| Molex | Works | Requires User-Agent header |\n| Renesas | Works | Direct download |\n| ON Semi | Works | Direct download |\n| NXP | Works | Direct download |\n| Diodes Inc | Works | Direct download |\n| Microchip | Works | Direct download via API URLs |\n| YAGEO, Samsung, Murata | Works | DigiKey-hosted PDFs (`mm.digikey.com`) |\n| Broadcom | Works* | Requires Playwright — `docs.broadcom.com` serves PDFs via JS download |\n| Espressif | Works* | Requires Playwright — download redirect needs JS execution |\n| Lattice | Mixed | Some URLs require cookies/auth |\n\n\\* Requires `playwright` package — falls back gracefully to user notification if not installed.\n\n### When Download Fails\n\nIf the script or inline download fails (exit code 1), **tell the user and provide the URL** so they can open it in a real browser. Some manufacturer sites (Lattice, TDK InvenSense) require interactive login, cookies, or CAPTCHA that even a headless browser can't handle. With Playwright installed, Broadcom and Espressif now download automatically.\n\nExample message to the user:\n> I couldn't download the datasheet for ICE40UP5K-SG48I automatically — Lattice's site requires browser authentication. Here's the direct link:\n> https://www.latticesemi.com/-/media/LatticeSemi/Documents/DataSheets/iCE/FPGA-DS-02008-1-9-iCE40-Ultra-Plus-Family-Data-Sheet.ashx\n>\n> You can open it in your browser and save it locally, then I can read and analyze it.\n\nThe `--json` output always includes the `datasheet_url` field even on failure, so you can extract the URL programmatically.\n\n### Manual Download Workflow\n\nIf the script isn't available or you need to do it inline:\n\n1. **Search for the part** using KeywordSearch or ProductDetails\n2. **Extract `DatasheetUrl`** from the API response\n3. **Normalize the URL** — if it starts with `//`, prepend `https:`. If it contains `ti.com/general/docs/suppproductinfo`, extract the `gotoUrl` query parameter.\n4. **Download with `requests`** (Python) or `wget`/`curl` with a browser User-Agent\n5. **Verify it's a PDF**: first 4 bytes should be `%PDF`\n\nIf the `DatasheetUrl` field is empty or all download methods fail:\n- Provide the URL to the user for manual browser download\n- Try the `/products/v4/search/{pn}/media` endpoint for alternative media links\n- Web search as a last resort: `\"\u003cMPN> datasheet filetype:pdf\"`\n\n### What to Extract from Datasheets\n\nWhen analyzing a datasheet for a KiCad design review (see `kicad` skill):\n- **Absolute maximum ratings** — voltage, current, temperature limits\n- **Recommended operating conditions** — typical operating ranges\n- **Pinout and pin descriptions** — verify against KiCad symbol\n- **Package dimensions** — verify against KiCad footprint\n- **Typical application circuit** — compare against the user's schematic\n- **Thermal characteristics** — θJA, θJC for power dissipation calculations\n- **Electrical characteristics** — key parameters (Vout, Iq, PSRR, etc.)\n\n## Tips\n\n- DigiKey PN suffixes: `-ND` standard, `-1-ND` cut tape, `-2-ND` digi-reel, `-6-ND` full reel\n- Use `ExcludeMarketPlace` filter to avoid third-party seller listings\n- Price breaks in `ProductVariations[].StandardPricing[]` — check `BreakQuantity` thresholds\n- Check `ProductStatus` and `Discontinued`/`EndOfLife` before selecting parts\n---","attachment_filenames":["scripts/fetch_datasheet_digikey.py","scripts/sync_datasheets_digikey.py"],"attachments":[{"filename":"scripts/fetch_datasheet_digikey.py","content":"#!/usr/bin/env python3\n\"\"\"Download a datasheet PDF from a manufacturer URL.\n\nHandles manufacturer-specific quirks:\n- TI: DigiKey returns JS-redirect URLs; this script extracts the direct PDF URL\n- Protocol-relative URLs (//mm.digikey.com/...): adds https: prefix\n- Microchip: Redirects to login; uses alternative URL patterns as fallback\n- Various manufacturers that block bare urllib: uses requests library with\n proper User-Agent (handles HTTP/2, redirects, and anti-bot checks)\n\nUsage:\n python3 fetch_datasheet_digikey.py \u003curl> [--output \u003cpath>]\n python3 fetch_datasheet_digikey.py --search \u003cMPN> [--output \u003cpath>]\n\nThe --search mode requires DIGIKEY_CLIENT_ID and DIGIKEY_CLIENT_SECRET\nenvironment variables.\n\nDependencies:\n - requests (pip install requests) — preferred, handles all manufacturer sites\n - Falls back to urllib if requests is not installed (some sites may fail)\n\nExit codes:\n 0 = success (PDF downloaded)\n 1 = download failed after all attempts\n 2 = search failed (API error or part not found)\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport urllib.parse\nimport urllib.request\n\n_USER_AGENT = \"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0\"\n\n# Try to import optional dependencies; graceful fallback if not installed\ntry:\n import requests as _requests\nexcept ImportError:\n _requests = None\n\ntry:\n from playwright.sync_api import sync_playwright as _sync_playwright\nexcept ImportError:\n _sync_playwright = None\n\n\ndef _friendly_filename(mpn: str, description: str = \"\") -> str:\n \"\"\"Build a human-readable filename (no extension) from MPN + description.\"\"\"\n def _sanitize(s):\n s = re.sub(r'[/\\\\:*?\"\u003c>|,;]', \"_\", s)\n s = re.sub(r\"\\s+\", \"_\", s)\n return re.sub(r\"_+\", \"_\", s).strip(\"_\")\n\n base = _sanitize(mpn)\n if not description:\n return base\n desc = description.strip()\n if len(desc) > 80:\n desc = desc[:77].rsplit(\" \", 1)[0]\n desc = _sanitize(desc)\n return f\"{base}_{desc}\" if desc else base\n\n\ndef _get_digikey_token() -> str | None:\n \"\"\"Get a DigiKey OAuth token, using a cached version if still valid.\n\n Caches the token to a temp file with a 9-minute TTL (tokens last 10 minutes).\n \"\"\"\n import tempfile\n import time\n\n client_id = os.environ.get(\"DIGIKEY_CLIENT_ID\", \"\")\n client_secret = os.environ.get(\"DIGIKEY_CLIENT_SECRET\", \"\")\n if not client_id or not client_secret:\n print(\"Error: DIGIKEY_CLIENT_ID and DIGIKEY_CLIENT_SECRET required\", file=sys.stderr)\n return None\n\n cache_path = os.path.join(tempfile.gettempdir(), \"digikey_token_cache.json\")\n\n # Check cache\n try:\n if os.path.exists(cache_path):\n with open(cache_path) as f:\n cached = json.load(f)\n if (cached.get(\"client_id\") == client_id\n and cached.get(\"expires_at\", 0) > time.time()):\n return cached[\"token\"]\n except (json.JSONDecodeError, OSError):\n pass\n\n # Fetch new token\n try:\n token_data = urllib.parse.urlencode({\n \"client_id\": client_id,\n \"client_secret\": client_secret,\n \"grant_type\": \"client_credentials\",\n }).encode()\n req = urllib.request.Request(\n \"https://api.digikey.com/v1/oauth2/token\",\n data=token_data,\n headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n )\n with urllib.request.urlopen(req, timeout=15) as resp:\n token_resp = json.loads(resp.read())\n token = token_resp.get(\"access_token\", \"\")\n if not token:\n print(\"Error: Failed to get OAuth token\", file=sys.stderr)\n return None\n\n # Cache with 9-minute TTL (token lasts 10 min, 1 min safety margin)\n try:\n with open(cache_path, \"w\") as f:\n json.dump({\n \"token\": token,\n \"client_id\": client_id,\n \"expires_at\": time.time() + 540,\n }, f)\n os.chmod(cache_path, 0o600)\n except OSError:\n pass # caching is best-effort\n\n return token\n except Exception as e:\n print(f\"Error: OAuth failed: {e}\", file=sys.stderr)\n return None\n\n\ndef search_digikey(mpn: str) -> dict | None:\n \"\"\"Search DigiKey API for a part, return first match with datasheet URL.\"\"\"\n client_id = os.environ.get(\"DIGIKEY_CLIENT_ID\", \"\")\n token = _get_digikey_token()\n if not token:\n return None\n\n # Search for the part\n try:\n search_body = json.dumps({\"Keywords\": mpn, \"Limit\": 3}).encode()\n req = urllib.request.Request(\n \"https://api.digikey.com/products/v4/search/keyword\",\n data=search_body,\n headers={\n \"Content-Type\": \"application/json\",\n \"X-DIGIKEY-Client-Id\": client_id,\n \"Authorization\": f\"Bearer {token}\",\n },\n )\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n except Exception as e:\n print(f\"Error: Search failed: {e}\", file=sys.stderr)\n return None\n\n products = data.get(\"Products\", [])\n if not products:\n print(f\"No results for '{mpn}'\", file=sys.stderr)\n return None\n\n # Prefer exact MPN match\n for p in products:\n if p.get(\"ManufacturerProductNumber\", \"\").upper().startswith(mpn.upper()):\n return p\n return products[0]\n\n\ndef normalize_url(url: str) -> str:\n \"\"\"Convert manufacturer-specific redirect URLs to direct PDF URLs.\n\n DigiKey's DatasheetUrl field often points to manufacturer redirect pages\n rather than direct PDFs. This function extracts the actual PDF URL.\n \"\"\"\n # Protocol-relative URLs from DigiKey (//mm.digikey.com/...)\n if url.startswith(\"//\"):\n url = \"https:\" + url\n\n # TI redirect: extract the gotoUrl parameter which contains the direct link\n if \"ti.com/general/docs/suppproductinfo\" in url:\n parsed = urllib.parse.urlparse(url)\n params = urllib.parse.parse_qs(parsed.query)\n goto = params.get(\"gotoUrl\", [\"\"])[0]\n if goto:\n return goto\n\n # Microchip redirect: extract the actual URL from the redirect wrapper\n if \"microchip.com/filehandler/redirect\" in url.lower():\n idx = url.find(\"?\")\n if idx >= 0:\n return url[idx + 1:]\n\n return url\n\n\ndef download_pdf(url: str, output_path: str) -> bool:\n \"\"\"Download a PDF from a URL, trying multiple methods.\n\n Strategy:\n 1. Try requests library (handles HTTP/2, redirects, all manufacturer sites)\n 2. Fall back to Python urllib (HTTP/1.1 only — some sites may timeout)\n\n Returns True if a valid PDF was downloaded.\n \"\"\"\n url = normalize_url(url)\n\n methods = []\n if _requests is not None:\n methods.append((\"requests\", _download_requests))\n methods.append((\"urllib\", _download_urllib))\n if _sync_playwright is not None:\n methods.append((\"playwright\", _download_playwright))\n\n for name, fn in methods:\n try:\n if fn(url, output_path):\n # Verify it's actually a PDF\n with open(output_path, \"rb\") as f:\n header = f.read(8)\n if header.startswith(b\"%PDF\"):\n size = os.path.getsize(output_path)\n print(f\"Downloaded {size:,} bytes via {name}: {output_path}\")\n return True\n else:\n # Not a PDF (probably HTML error page or JS redirect)\n os.remove(output_path)\n except Exception:\n if os.path.exists(output_path):\n os.remove(output_path)\n continue\n\n return False\n\n\ndef _download_requests(url: str, output_path: str) -> bool:\n \"\"\"Download using the requests library (handles HTTP/2, all manufacturers).\"\"\"\n resp = _requests.get(\n url,\n headers={\"User-Agent\": _USER_AGENT},\n timeout=20,\n allow_redirects=True,\n )\n resp.raise_for_status()\n if len(resp.content) == 0:\n return False\n with open(output_path, \"wb\") as f:\n f.write(resp.content)\n return True\n\n\ndef _download_urllib(url: str, output_path: str) -> bool:\n \"\"\"Download using Python urllib (fallback, HTTP/1.1 only).\"\"\"\n req = urllib.request.Request(url, headers={\"User-Agent\": _USER_AGENT})\n with urllib.request.urlopen(req, timeout=20) as resp:\n with open(output_path, \"wb\") as f:\n shutil.copyfileobj(resp, f)\n return os.path.exists(output_path) and os.path.getsize(output_path) > 0\n\n\ndef _download_playwright(url: str, output_path: str) -> bool:\n \"\"\"Download using Playwright headless browser.\n\n Last-resort fallback for sites that require JavaScript execution\n (Broadcom doc viewer, Espressif, etc.). Handles two patterns:\n 1. Page triggers a file download (intercepted via expect_download)\n 2. Page navigates directly to a PDF (read from response body)\n \"\"\"\n with _sync_playwright() as p:\n browser = p.chromium.launch(headless=True)\n page = browser.new_page()\n try:\n # First try: expect a download event (some sites trigger JS downloads)\n try:\n with page.expect_download(timeout=15000) as dl_info:\n page.goto(url, timeout=15000, wait_until=\"domcontentloaded\")\n download = dl_info.value\n download.save_as(output_path)\n return os.path.exists(output_path) and os.path.getsize(output_path) > 0\n except Exception:\n pass\n\n # Second try: navigate and read the response body directly\n resp = page.goto(url, timeout=20000, wait_until=\"domcontentloaded\")\n if resp is None:\n return False\n body = resp.body()\n if body and body[:4] == b\"%PDF\":\n with open(output_path, \"wb\") as f:\n f.write(body)\n return True\n return False\n finally:\n page.close()\n browser.close()\n\n\ndef try_alternative_sources(mpn: str, output_path: str) -> bool:\n \"\"\"Try downloading from known mirror/alternative datasheet sources.\n\n When the primary manufacturer URL fails (Microchip 403, ST timeout),\n try alternative sources that host the same datasheets.\n \"\"\"\n alternatives = []\n mpn_upper = mpn.upper()\n\n # Microchip: try direct product document URL\n if any(x in mpn_upper for x in (\"ATMEGA\", \"ATTINY\", \"PIC\", \"SAMD\", \"SAM\")):\n alternatives.append(\n f\"https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/{mpn}-DataSheet.pdf\"\n )\n\n for alt_url in alternatives:\n if download_pdf(alt_url, output_path):\n return True\n\n return False\n\n\ndef _extract_pdf_text(pdf_path: str, max_pages: int = 3) -> str:\n \"\"\"Extract text from the first few pages of a PDF.\n\n Tries pdftotext (poppler-utils) first for best quality, then falls\n back to scanning raw PDF bytes for ASCII strings.\n \"\"\"\n # Strategy 1: pdftotext (best quality, handles all encodings)\n try:\n result = subprocess.run(\n [\"pdftotext\", \"-l\", str(max_pages), pdf_path, \"-\"],\n capture_output=True, text=True, timeout=10,\n )\n if result.returncode == 0 and result.stdout.strip():\n return result.stdout\n except (FileNotFoundError, subprocess.TimeoutExpired):\n pass\n\n # Strategy 2: raw byte scanning (no dependencies needed)\n try:\n with open(pdf_path, \"rb\") as f:\n raw = f.read(200_000) # first ~200KB covers first few pages\n # Extract readable ASCII sequences (4+ chars)\n strings = re.findall(rb\"[\\x20-\\x7e]{4,}\", raw)\n return \" \".join(s.decode(\"ascii\", errors=\"ignore\") for s in strings)\n except Exception:\n return \"\"\n\n\ndef verify_datasheet(\n pdf_path: str,\n mpn: str,\n description: str = \"\",\n manufacturer: str = \"\",\n) -> dict:\n \"\"\"Verify a downloaded PDF is actually the correct datasheet.\n\n Extracts text from the first few pages and checks for the MPN,\n manufacturer, and description keywords. Returns a dict with:\n - verified: bool (True if MPN found in text)\n - confidence: \"verified\" | \"likely\" | \"unverified\" | \"wrong\"\n - mpn_found: bool\n - manufacturer_found: bool\n - keyword_hits: int (how many description keywords matched)\n - keyword_total: int\n - details: str (human-readable explanation)\n \"\"\"\n text = _extract_pdf_text(pdf_path)\n if not text or len(text) \u003c 50:\n return {\n \"verified\": False,\n \"confidence\": \"unverified\",\n \"mpn_found\": False,\n \"manufacturer_found\": False,\n \"keyword_hits\": 0,\n \"keyword_total\": 0,\n \"details\": \"Could not extract text from PDF\",\n }\n\n text_upper = text.upper()\n\n # Check for MPN — try exact, then base MPN without common suffixes\n mpn_upper = mpn.upper()\n mpn_found = mpn_upper in text_upper\n\n if not mpn_found:\n # Strip common ordering suffixes and try again\n # e.g., TPS61023DRLR -> TPS61023, BSS138LT1G -> BSS138\n base_mpn = re.sub(\n r\"(DRLR|DRL|DGKR|DGK|DCKR|DCK|DBVR|DBV|PWPR|PWP|RGER|RGE|\"\n r\"RGTR|RGT|NRND|TR|CT|ND|LT1G|LT3G|BK|PBF|-ND)$\",\n \"\", mpn_upper,\n )\n if base_mpn and len(base_mpn) >= 4 and base_mpn != mpn_upper:\n mpn_found = base_mpn in text_upper\n\n # Check manufacturer name\n mfg_found = False\n if manufacturer:\n mfg_upper = manufacturer.upper()\n # Try full name and common short forms\n mfg_found = mfg_upper in text_upper\n if not mfg_found:\n # Try first word (e.g., \"Texas\" from \"Texas Instruments\")\n first_word = mfg_upper.split()[0] if \" \" in mfg_upper else \"\"\n if first_word and len(first_word) >= 4:\n mfg_found = first_word in text_upper\n\n # Check description keywords\n keywords = []\n if description:\n # Extract meaningful words from description (skip noise)\n skip = {\"the\", \"a\", \"an\", \"for\", \"and\", \"or\", \"with\", \"in\", \"to\",\n \"of\", \"at\", \"by\", \"on\", \"no\", \"w\", \"smd\", \"smt\"}\n for word in re.split(r\"[\\s/,_-]+\", description):\n w = word.strip().upper()\n if len(w) >= 3 and w not in skip and not re.match(r\"^\\d+$\", w):\n keywords.append(w)\n\n keyword_hits = sum(1 for kw in keywords if kw in text_upper) if keywords else 0\n keyword_total = len(keywords)\n\n # Determine confidence level\n if mpn_found:\n confidence = \"verified\"\n details = f\"MPN '{mpn}' found in PDF text\"\n elif mfg_found and keyword_hits >= max(1, keyword_total // 2):\n confidence = \"likely\"\n details = (f\"MPN not found but manufacturer '{manufacturer}' present \"\n f\"with {keyword_hits}/{keyword_total} description keywords\")\n elif keyword_hits >= max(2, keyword_total * 2 // 3):\n confidence = \"likely\"\n details = f\"{keyword_hits}/{keyword_total} description keywords found\"\n elif keyword_hits == 0 and keyword_total >= 3 and not mfg_found:\n confidence = \"wrong\"\n details = (f\"No MPN, manufacturer, or description keywords found in PDF \"\n f\"(0/{keyword_total} keywords)\")\n else:\n confidence = \"unverified\"\n details = (f\"MPN not found; {keyword_hits}/{keyword_total} keywords, \"\n f\"manufacturer {'found' if mfg_found else 'not found'}\")\n\n return {\n \"verified\": mpn_found,\n \"confidence\": confidence,\n \"mpn_found\": mpn_found,\n \"manufacturer_found\": mfg_found,\n \"keyword_hits\": keyword_hits,\n \"keyword_total\": keyword_total,\n \"details\": details,\n }\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Download a datasheet PDF\")\n group = parser.add_mutually_exclusive_group(required=True)\n group.add_argument(\"url\", nargs=\"?\", help=\"Direct URL to the PDF\")\n group.add_argument(\"--search\", metavar=\"MPN\", help=\"Search DigiKey by MPN and download the datasheet\")\n parser.add_argument(\"--output\", \"-o\", help=\"Output file path (default: \u003cMPN>.pdf in current directory)\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output result as JSON (for script integration)\")\n args = parser.parse_args()\n\n if args.search:\n product = search_digikey(args.search)\n if not product:\n if args.json:\n json.dump({\"success\": False, \"error\": \"Part not found\"}, sys.stdout)\n sys.exit(2)\n\n mpn = product.get(\"ManufacturerProductNumber\", args.search)\n desc = product.get(\"Description\", {}).get(\"ProductDescription\", \"\")\n ds_url = product.get(\"DatasheetUrl\", \"\")\n output_path = args.output or (_friendly_filename(mpn, desc) + \".pdf\")\n\n if args.json:\n result = {\n \"mpn\": mpn,\n \"manufacturer\": product.get(\"Manufacturer\", {}).get(\"Name\", \"\"),\n \"description\": product.get(\"Description\", {}).get(\"ProductDescription\", \"\"),\n \"datasheet_url\": ds_url,\n }\n\n if not ds_url:\n print(f\"No datasheet URL for {mpn}\", file=sys.stderr)\n if args.json:\n result[\"success\"] = False\n result[\"error\"] = \"No datasheet URL in DigiKey listing\"\n json.dump(result, sys.stdout)\n sys.exit(2)\n\n if download_pdf(ds_url, output_path):\n vr = verify_datasheet(output_path, mpn, desc,\n product.get(\"Manufacturer\", {}).get(\"Name\", \"\"))\n if vr[\"confidence\"] == \"wrong\":\n print(f\"WARNING: Downloaded PDF may be wrong datasheet for {mpn}\",\n file=sys.stderr)\n print(f\" {vr['details']}\", file=sys.stderr)\n elif vr[\"confidence\"] == \"unverified\":\n print(f\" Verification inconclusive for {mpn}: {vr['details']}\",\n file=sys.stderr)\n if args.json:\n result[\"success\"] = True\n result[\"output\"] = output_path\n result[\"verification\"] = vr\n json.dump(result, sys.stdout)\n sys.exit(0)\n\n # Try alternative sources\n print(f\"Primary URL failed, trying alternatives for {mpn}...\", file=sys.stderr)\n if try_alternative_sources(mpn, output_path):\n vr = verify_datasheet(output_path, mpn, desc,\n product.get(\"Manufacturer\", {}).get(\"Name\", \"\"))\n if vr[\"confidence\"] == \"wrong\":\n print(f\"WARNING: Downloaded PDF may be wrong datasheet for {mpn}\",\n file=sys.stderr)\n print(f\" {vr['details']}\", file=sys.stderr)\n if args.json:\n result[\"success\"] = True\n result[\"output\"] = output_path\n result[\"source\"] = \"alternative\"\n result[\"verification\"] = vr\n json.dump(result, sys.stdout)\n sys.exit(0)\n\n print(f\"Failed to download datasheet for {mpn}\", file=sys.stderr)\n if args.json:\n result[\"success\"] = False\n result[\"error\"] = \"All download methods failed\"\n json.dump(result, sys.stdout)\n sys.exit(1)\n\n else:\n # Direct URL mode\n url = args.url\n output_path = args.output or \"datasheet.pdf\"\n\n if download_pdf(url, output_path):\n if args.json:\n json.dump({\"success\": True, \"output\": output_path, \"url\": url}, sys.stdout)\n sys.exit(0)\n else:\n print(f\"Failed to download PDF from {url}\", file=sys.stderr)\n if args.json:\n json.dump({\"success\": False, \"url\": url, \"error\": \"Download failed\"}, sys.stdout)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":20141,"content_sha256":"ce4f4756f6883696d1c1f7daaaf8cdc59751d0d7a194b15d44ce2a9c8baf58f0"},{"filename":"scripts/sync_datasheets_digikey.py","content":"#!/usr/bin/env python3\n\"\"\"Sync a local datasheets directory for a KiCad project.\n\nExtracts components with MPNs from a KiCad schematic (or pre-computed\nanalyzer JSON), searches DigiKey for datasheet URLs, downloads missing\nPDFs, and maintains a manifest.json file (formerly index.json — the legacy name\nis still read for backward compat on existing projects).\n\nThe manifest.json tracks download status so subsequent runs only fetch new\nor changed parts. The kicad skill can read this index to cross-reference\ndatasheets during design review.\n\nUsage:\n python3 sync_datasheets_digikey.py \u003cfile.kicad_sch>\n python3 sync_datasheets_digikey.py \u003canalyzer_output.json> --output ./datasheets\n python3 sync_datasheets_digikey.py \u003cfile.kicad_sch> --force # retry failures\n python3 sync_datasheets_digikey.py \u003cfile.kicad_sch> --dry-run # preview only\n python3 sync_datasheets_digikey.py --mpn-list mpns.txt --dry-run\n python3 sync_datasheets_digikey.py --mpn-list mpns.txt --output ./datasheets\n\nRequires DIGIKEY_CLIENT_ID and DIGIKEY_CLIENT_SECRET environment variables.\n\nDependencies:\n - requests (pip install requests) — strongly recommended\n - playwright (pip install playwright && playwright install chromium) — optional\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport threading\nimport time\nimport urllib.parse\nimport urllib.request\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n# Import download functions from sibling script\nsys.path.insert(0, str(Path(__file__).parent))\nfrom fetch_datasheet_digikey import download_pdf, normalize_url, try_alternative_sources, verify_datasheet\n\n# ---------------------------------------------------------------------------\n# MPN filtering — distinguish real manufacturer part numbers from generic values\n# ---------------------------------------------------------------------------\n\n# Matches generic passive values that someone typed into the MPN field\n_GENERIC_VALUE_RE = re.compile(\n r\"^[\\d.]+\\s*[pnuμmkMGR]?[FHΩRfhω]?$\" # 100nF, 10K, 4.7uF, 100R\n r\"|^[\\d.]+\\s*[kKmM]?[Ωω]?$\" # 10K, 4.7k\n r\"|^[\\d.]+\\s*[pnuμm]?[Ff]$\" # 100pF, 10uF\n r\"|^[\\d.]+\\s*[pnuμm]?[Hh]$\" # 10uH\n r\"|^[\\d.]+%$\" # 1%\n r\"|^DNP$|^NC$|^N/?A$\",\n re.IGNORECASE,\n)\n\n# Component types that never have useful datasheets\n_SKIP_TYPES = {\n \"test_point\", \"mounting_hole\", \"fiducial\", \"graphic\",\n \"jumper\", \"net_tie\", \"mechanical\",\n}\n\n\ndef is_real_mpn(mpn: str) -> bool:\n \"\"\"Return True if the string looks like a real manufacturer part number.\"\"\"\n mpn = mpn.strip()\n if not mpn or len(mpn) \u003c 3:\n return False\n if _GENERIC_VALUE_RE.match(mpn):\n return False\n # Must contain both letters and digits (real MPNs always do)\n has_letter = any(c.isalpha() for c in mpn)\n has_digit = any(c.isdigit() for c in mpn)\n if not (has_letter and has_digit):\n return False\n return True\n\n\n# ---------------------------------------------------------------------------\n# OAuth token management — fetch once, reuse across all API calls\n# ---------------------------------------------------------------------------\n\ndef get_oauth_token() -> tuple[str, str] | None:\n \"\"\"Get DigiKey OAuth token. Returns (token, client_id) or None.\"\"\"\n client_id = os.environ.get(\"DIGIKEY_CLIENT_ID\", \"\")\n client_secret = os.environ.get(\"DIGIKEY_CLIENT_SECRET\", \"\")\n if not client_id or not client_secret:\n print(\"Error: DIGIKEY_CLIENT_ID and DIGIKEY_CLIENT_SECRET environment variables required.\",\n file=sys.stderr)\n print(\" Get credentials at developer.digikey.com → My Apps → Create App\",\n file=sys.stderr)\n return None\n\n try:\n token_data = urllib.parse.urlencode({\n \"client_id\": client_id,\n \"client_secret\": client_secret,\n \"grant_type\": \"client_credentials\",\n }).encode()\n req = urllib.request.Request(\n \"https://api.digikey.com/v1/oauth2/token\",\n data=token_data,\n headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n )\n with urllib.request.urlopen(req, timeout=15) as resp:\n token_resp = json.loads(resp.read())\n token = token_resp.get(\"access_token\", \"\")\n if not token:\n print(\"Error: Failed to get OAuth token\", file=sys.stderr)\n return None\n return token, client_id\n except Exception as e:\n print(f\"Error: OAuth failed: {e}\", file=sys.stderr)\n return None\n\n\ndef search_digikey_with_token(mpn: str, token: str, client_id: str) -> dict | None:\n \"\"\"Search DigiKey API using a pre-fetched OAuth token.\"\"\"\n try:\n search_body = json.dumps({\"Keywords\": mpn, \"Limit\": 3}).encode()\n req = urllib.request.Request(\n \"https://api.digikey.com/products/v4/search/keyword\",\n data=search_body,\n headers={\n \"Content-Type\": \"application/json\",\n \"X-DIGIKEY-Client-Id\": client_id,\n \"Authorization\": f\"Bearer {token}\",\n },\n )\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n except urllib.error.HTTPError as e:\n if e.code == 429:\n print(f\" Rate limited, waiting 10s...\", file=sys.stderr)\n time.sleep(10)\n return search_digikey_with_token(mpn, token, client_id)\n if e.code == 401:\n return None # Token expired — caller should refresh\n print(f\" Search failed for '{mpn}': HTTP {e.code}\", file=sys.stderr)\n return None\n except Exception as e:\n print(f\" Search failed for '{mpn}': {e}\", file=sys.stderr)\n return None\n\n products = data.get(\"Products\", [])\n if not products:\n return None\n\n # Prefer exact MPN match\n for p in products:\n if p.get(\"ManufacturerProductNumber\", \"\").upper().startswith(mpn.upper()):\n return p\n return products[0]\n\n\n# ---------------------------------------------------------------------------\n# Filename sanitization\n# ---------------------------------------------------------------------------\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Convert a string to a safe filename component (without extension).\"\"\"\n # Replace filesystem-unsafe characters and commas\n name = re.sub(r'[/\\\\:*?\"\u003c>|,;]', \"_\", name)\n # Collapse whitespace and underscores\n name = re.sub(r\"\\s+\", \"_\", name)\n name = re.sub(r\"_+\", \"_\", name).strip(\"_\")\n # Truncate\n if len(name) > 200:\n name = name[:200]\n return name\n\n\ndef friendly_filename(mpn: str, description: str = \"\", manufacturer: str = \"\") -> str:\n \"\"\"Build a human-readable filename from MPN and description.\n\n Examples:\n TPS61023DRLR_Boost_Converter.pdf\n BSS138LT1G_MOSFET_N-CH_50V_200mA.pdf\n GRPB032VWQS-RC_Conn_Header_SMD_6pos.pdf\n\n Falls back to just the sanitized MPN if no description is available.\n \"\"\"\n base = sanitize_filename(mpn)\n if not description:\n return base\n\n # Clean up the description: trim common noise\n desc = description.strip()\n # Remove trailing manufacturer name if it's just repeated\n if manufacturer and desc.lower().endswith(manufacturer.lower()):\n desc = desc[: -len(manufacturer)].strip().rstrip(\",\").strip()\n # Truncate long descriptions to keep filenames reasonable\n if len(desc) > 80:\n desc = desc[:77].rsplit(\"_\", 1)[0].rsplit(\" \", 1)[0]\n desc = sanitize_filename(desc)\n if not desc:\n return base\n\n return f\"{base}_{desc}\"\n\n\n# ---------------------------------------------------------------------------\n# Manifest management (manifest.json; legacy index.json read for backward compat)\n# ---------------------------------------------------------------------------\n\nMANIFEST_FILENAME = \"manifest.json\"\nLEGACY_MANIFEST_FILENAME = \"index.json\"\n\n\ndef _manifest_path(out_dir: Path) -> Path:\n \"\"\"Return manifest path, preferring manifest.json but falling back to index.json.\"\"\"\n new = out_dir / MANIFEST_FILENAME\n old = out_dir / LEGACY_MANIFEST_FILENAME\n if new.exists() or not old.exists():\n return new\n return old\n\n\ndef load_index(path: Path) -> dict:\n \"\"\"Load existing manifest.json (or legacy index.json) or return empty.\"\"\"\n path = _manifest_path(path.parent) if path.name in (MANIFEST_FILENAME, LEGACY_MANIFEST_FILENAME) else path\n if path.exists():\n try:\n with open(path, \"r\") as f:\n return json.load(f)\n except (json.JSONDecodeError, OSError):\n pass\n return {\"schematic\": \"\", \"last_sync\": \"\", \"parts\": {}}\n\n\ndef save_index(path: Path, index: dict):\n \"\"\"Write manifest atomically. Always writes manifest.json; removes any\n legacy index.json sibling after a successful write.\"\"\"\n parent = path.parent\n parent.mkdir(parents=True, exist_ok=True)\n new_path = parent / MANIFEST_FILENAME\n tmp = new_path.with_suffix(\".tmp\")\n with open(tmp, \"w\") as f:\n json.dump(index, f, indent=2)\n tmp.rename(new_path)\n old_path = parent / LEGACY_MANIFEST_FILENAME\n if old_path.exists() and old_path != new_path:\n try:\n old_path.unlink()\n except OSError:\n pass\n\n\n# ---------------------------------------------------------------------------\n# Schematic analysis — run analyzer or load pre-computed JSON\n# ---------------------------------------------------------------------------\n\ndef get_analyzer_output(input_path: Path) -> dict | None:\n \"\"\"Get analyzer output, either by running the analyzer or loading JSON.\"\"\"\n if input_path.suffix == \".json\":\n with open(input_path, \"r\") as f:\n return json.load(f)\n\n if input_path.suffix in (\".kicad_sch\", \".sch\"):\n # Try importing the analyzer directly\n kicad_scripts = Path(__file__).resolve().parent.parent.parent / \"kicad\" / \"scripts\"\n if kicad_scripts.exists():\n sys.path.insert(0, str(kicad_scripts))\n try:\n from analyze_schematic import analyze_schematic\n return analyze_schematic(str(input_path))\n except Exception as e:\n print(f\" Analyzer import failed ({e}), trying subprocess...\",\n file=sys.stderr)\n\n # Fall back to subprocess\n analyzer = kicad_scripts / \"analyze_schematic.py\"\n if not analyzer.exists():\n print(f\"Error: Cannot find analyze_schematic.py at {analyzer}\",\n file=sys.stderr)\n return None\n try:\n result = subprocess.run(\n [sys.executable, str(analyzer), str(input_path), \"--compact\"],\n capture_output=True, text=True, timeout=120,\n )\n if result.returncode != 0:\n print(f\"Error: Analyzer failed: {result.stderr[:500]}\",\n file=sys.stderr)\n return None\n return json.loads(result.stdout)\n except Exception as e:\n print(f\"Error: Failed to run analyzer: {e}\", file=sys.stderr)\n return None\n\n print(f\"Error: Unsupported input file type: {input_path.suffix}\",\n file=sys.stderr)\n return None\n\n\n# ---------------------------------------------------------------------------\n# MPN extraction from BOM\n# ---------------------------------------------------------------------------\n\ndef extract_parts(analyzer_output: dict) -> list[dict]:\n \"\"\"Extract unique parts with real MPNs or distributor PNs from analyzer BOM output.\n\n A part is included if it has at least one of: a real MPN, a DigiKey PN,\n a Mouser PN, or an LCSC PN. Users set up their KiCad projects differently —\n some only have MPNs, some only have distributor PNs, some have both.\n \"\"\"\n bom = analyzer_output.get(\"bom\", [])\n parts = []\n\n for entry in bom:\n if entry.get(\"dnp\"):\n continue\n if entry.get(\"type\", \"\") in _SKIP_TYPES:\n continue\n\n mpn = entry.get(\"mpn\", \"\").strip()\n digikey_pn = entry.get(\"digikey\", \"\").strip()\n mouser_pn = entry.get(\"mouser\", \"\").strip()\n lcsc_pn = entry.get(\"lcsc\", \"\").strip()\n\n # Need at least one identifier to search for a datasheet\n has_mpn = is_real_mpn(mpn)\n has_distributor_pn = bool(digikey_pn or mouser_pn or lcsc_pn)\n if not has_mpn and not has_distributor_pn:\n continue\n\n parts.append({\n \"mpn\": mpn if has_mpn else \"\",\n \"manufacturer\": entry.get(\"manufacturer\", \"\"),\n \"value\": entry.get(\"value\", \"\"),\n \"description\": entry.get(\"description\", \"\"),\n \"datasheet\": entry.get(\"datasheet\", \"\"),\n \"references\": entry.get(\"references\", []),\n \"type\": entry.get(\"type\", \"\"),\n \"digikey\": digikey_pn,\n \"mouser\": mouser_pn,\n \"lcsc\": lcsc_pn,\n })\n\n return parts\n\n\ndef load_mpn_list(path: Path) -> list[dict]:\n \"\"\"Read MPNs from a text file, one per line (KH-312).\n\n Skips blank lines, full-line comments (``# ...``), and inline\n ``# ...`` comments. Filters non-MPN strings via ``is_real_mpn()``\n and de-duplicates. Returns minimal part dicts compatible with\n ``extract_parts()`` output — distributor PN fields are empty since\n MPN-list mode drives searches via MPN lookup only.\n\n Intended for batch workflows (harness, bulk datasheet seeding)\n that don't have a KiCad project to point at.\n\n Note: MPNs containing a literal ``#`` character are not supported\n (they would be silently truncated by the inline-comment stripper).\n Use the positional schematic/JSON input for such parts instead.\n \"\"\"\n parts: list[dict] = []\n seen: set[str] = set()\n with open(path, \"r\", encoding=\"utf-8\") as f:\n for raw in f:\n line = raw.strip()\n if not line or line.startswith(\"#\"):\n continue\n # Strip inline comments\n if \"#\" in line:\n line = line.split(\"#\", 1)[0].strip()\n if not line:\n continue\n if not is_real_mpn(line):\n print(f\" Skipping '{line}': doesn't look like a real MPN\",\n file=sys.stderr)\n continue\n if line in seen:\n continue\n seen.add(line)\n parts.append({\n \"mpn\": line,\n \"manufacturer\": \"\",\n \"value\": \"\",\n \"description\": \"\",\n \"datasheet\": \"\",\n \"references\": [],\n \"type\": \"\",\n \"digikey\": \"\",\n \"mouser\": \"\",\n \"lcsc\": \"\",\n })\n return parts\n\n\n# ---------------------------------------------------------------------------\n# Core sync logic\n# ---------------------------------------------------------------------------\n\ndef sync_one_part(\n part: dict,\n output_dir: Path,\n token: str,\n client_id: str,\n index: dict,\n delay: float,\n) -> dict:\n \"\"\"Download datasheet for one part. Returns updated manifest entry.\"\"\"\n mpn = part[\"mpn\"]\n digikey_pn = part.get(\"digikey\", \"\")\n now = datetime.now(timezone.utc).isoformat()\n\n # Use MPN for display/filename if available, otherwise fall back to DK PN\n display_pn = mpn or digikey_pn\n\n # Build a friendly filename — may be refined later if DigiKey provides\n # a better description than the schematic had\n desc = part.get(\"description\", \"\")\n mfg = part.get(\"manufacturer\", \"\")\n filename = friendly_filename(display_pn, desc, mfg) + \".pdf\"\n output_path = output_dir / filename\n\n # Strategy 1: Try the datasheet URL from the schematic itself\n schematic_url = part.get(\"datasheet\", \"\")\n if schematic_url and schematic_url != \"~\" and \"://\" in schematic_url:\n print(f\" Trying schematic URL...\", file=sys.stderr)\n if download_pdf(schematic_url, str(output_path)):\n size = os.path.getsize(str(output_path))\n vr = verify_datasheet(str(output_path), display_pn, desc, mfg)\n if vr[\"confidence\"] == \"wrong\":\n print(f\" WARNING: PDF may be wrong datasheet — {vr['details']}\",\n file=sys.stderr)\n result = {\n \"file\": filename,\n \"manufacturer\": mfg,\n \"description\": desc,\n \"value\": part[\"value\"],\n \"datasheet_url\": schematic_url,\n \"downloaded_date\": now,\n \"source\": \"schematic\",\n \"status\": \"ok\",\n \"references\": part[\"references\"],\n \"size_bytes\": size,\n \"verification\": vr[\"confidence\"],\n }\n if vr[\"confidence\"] == \"wrong\":\n result[\"verification_details\"] = vr[\"details\"]\n return result\n\n # Strategy 2: Search DigiKey API\n # Prefer DigiKey PN (exact match, no ambiguity) over MPN keyword search\n search_term = digikey_pn or mpn\n time.sleep(delay) # Rate limit\n print(f\" Searching DigiKey for '{search_term}'...\", file=sys.stderr)\n product = search_digikey_with_token(search_term, token, client_id)\n\n # If DK PN search failed but we also have an MPN, try that\n if product is None and digikey_pn and mpn:\n time.sleep(delay)\n print(f\" DK PN not found, trying MPN '{mpn}'...\", file=sys.stderr)\n product = search_digikey_with_token(mpn, token, client_id)\n\n if product is None:\n return {\n \"manufacturer\": part[\"manufacturer\"],\n \"description\": part.get(\"description\", \"\"),\n \"value\": part[\"value\"],\n \"references\": part[\"references\"],\n \"status\": \"not_found\",\n \"error\": f\"No DigiKey results for '{search_term}'\" + (f\" or '{mpn}'\" if digikey_pn and mpn else \"\"),\n \"last_attempt\": now,\n }\n\n ds_url = product.get(\"DatasheetUrl\", \"\")\n dk_mpn = product.get(\"ManufacturerProductNumber\", mpn)\n dk_mfg = product.get(\"Manufacturer\", {}).get(\"Name\", part[\"manufacturer\"])\n dk_desc = product.get(\"Description\", {}).get(\"ProductDescription\", \"\")\n\n # If DigiKey returned the real MPN, use it (better than a DK PN for filenames)\n if dk_mpn and not mpn:\n display_pn = dk_mpn\n filename = friendly_filename(display_pn, dk_desc or desc, dk_mfg) + \".pdf\"\n output_path = output_dir / filename\n elif dk_desc:\n # Rebuild filename with the richer DigiKey description\n filename = friendly_filename(display_pn, dk_desc, dk_mfg) + \".pdf\"\n output_path = output_dir / filename\n\n if not ds_url:\n return {\n \"manufacturer\": dk_mfg,\n \"description\": dk_desc or part.get(\"description\", \"\"),\n \"value\": part[\"value\"],\n \"references\": part[\"references\"],\n \"status\": \"no_datasheet\",\n \"error\": \"DigiKey listing has no datasheet URL\",\n \"last_attempt\": now,\n }\n\n # Strategy 3: Download from DigiKey's datasheet URL\n effective_desc = dk_desc or part.get(\"description\", \"\")\n print(f\" Downloading from {ds_url[:80]}...\", file=sys.stderr)\n if download_pdf(ds_url, str(output_path)):\n size = os.path.getsize(str(output_path))\n vr = verify_datasheet(str(output_path), dk_mpn or display_pn, effective_desc, dk_mfg)\n if vr[\"confidence\"] == \"wrong\":\n print(f\" WARNING: PDF may be wrong datasheet — {vr['details']}\",\n file=sys.stderr)\n result = {\n \"file\": filename,\n \"manufacturer\": dk_mfg,\n \"description\": effective_desc,\n \"value\": part[\"value\"],\n \"datasheet_url\": ds_url,\n \"downloaded_date\": now,\n \"source\": \"digikey\",\n \"status\": \"ok\",\n \"references\": part[\"references\"],\n \"size_bytes\": size,\n \"verification\": vr[\"confidence\"],\n }\n if vr[\"confidence\"] == \"wrong\":\n result[\"verification_details\"] = vr[\"details\"]\n return result\n\n # Strategy 4: Try alternative sources\n print(f\" Primary failed, trying alternatives...\", file=sys.stderr)\n if try_alternative_sources(dk_mpn, str(output_path)):\n size = os.path.getsize(str(output_path))\n vr = verify_datasheet(str(output_path), dk_mpn or display_pn, effective_desc, dk_mfg)\n if vr[\"confidence\"] == \"wrong\":\n print(f\" WARNING: PDF may be wrong datasheet — {vr['details']}\",\n file=sys.stderr)\n result = {\n \"file\": filename,\n \"manufacturer\": dk_mfg,\n \"description\": effective_desc,\n \"value\": part[\"value\"],\n \"datasheet_url\": ds_url,\n \"downloaded_date\": now,\n \"source\": \"alternative\",\n \"status\": \"ok\",\n \"references\": part[\"references\"],\n \"size_bytes\": size,\n \"verification\": vr[\"confidence\"],\n }\n if vr[\"confidence\"] == \"wrong\":\n result[\"verification_details\"] = vr[\"details\"]\n return result\n\n return {\n \"manufacturer\": dk_mfg,\n \"description\": dk_desc or part.get(\"description\", \"\"),\n \"value\": part[\"value\"],\n \"datasheet_url\": ds_url,\n \"references\": part[\"references\"],\n \"status\": \"failed\",\n \"error\": \"All download methods failed\",\n \"last_attempt\": now,\n }\n\n\ndef sync_datasheets(\n input_path: str | None = None,\n output_dir: str | None = None,\n force: bool = False,\n force_all: bool = False,\n delay: float = 1.0,\n parallel: int = 1,\n dry_run: bool = False,\n as_json: bool = False,\n mpn_list: str | None = None,\n) -> dict:\n \"\"\"Main sync function. Returns summary dict.\"\"\"\n if input_path is None and mpn_list is None:\n return {\"error\": \"Must provide either input_path or mpn_list\"}\n if input_path is not None and mpn_list is not None:\n return {\"error\": \"Cannot provide both input_path and mpn_list\"}\n\n # Determine output directory\n if output_dir:\n out_dir = Path(output_dir)\n elif input_path:\n out_dir = Path(input_path).resolve().parent / \"datasheets\"\n else:\n # MPN-list mode without explicit --output: use cwd/datasheets\n out_dir = Path.cwd() / \"datasheets\"\n out_dir.mkdir(parents=True, exist_ok=True)\n\n index_path = out_dir / MANIFEST_FILENAME\n index = load_index(index_path)\n\n # Source parts: either analyzer BOM or plain MPN list\n if mpn_list:\n mpn_list_path = Path(mpn_list).resolve()\n print(f\"Loading MPNs from {mpn_list_path.name}...\", file=sys.stderr)\n parts = load_mpn_list(mpn_list_path)\n print(f\"Loaded {len(parts)} unique MPNs\", file=sys.stderr)\n skipped_no_id = 0\n else:\n resolved_input = Path(input_path).resolve()\n print(f\"Analyzing {resolved_input.name}...\", file=sys.stderr)\n analyzer_output = get_analyzer_output(resolved_input)\n if analyzer_output is None:\n return {\"error\": \"Failed to analyze schematic\"}\n\n # Extract parts with real MPNs\n parts = extract_parts(analyzer_output)\n all_bom = analyzer_output.get(\"bom\", [])\n skipped_no_id = sum(\n 1 for e in all_bom\n if not e.get(\"dnp\") and e.get(\"type\", \"\") not in _SKIP_TYPES\n and not is_real_mpn(e.get(\"mpn\", \"\"))\n and not e.get(\"digikey\", \"\").strip()\n and not e.get(\"mouser\", \"\").strip()\n and not e.get(\"lcsc\", \"\").strip()\n )\n\n print(f\"Found {len(parts)} unique parts with part numbers \"\n f\"({skipped_no_id} skipped without any identifier)\", file=sys.stderr)\n\n # Determine what needs processing\n to_download = []\n already_present = []\n skipped_failed = []\n\n for part in parts:\n # Use MPN as index key if available, otherwise distributor PN\n part_key = part[\"mpn\"] or part.get(\"digikey\", \"\") or part.get(\"mouser\", \"\") or part.get(\"lcsc\", \"\")\n part[\"_key\"] = part_key\n existing = index.get(\"parts\", {}).get(part_key, {})\n status = existing.get(\"status\", \"\")\n\n if status == \"ok\":\n old_file = existing.get(\"file\", \"\")\n # Verify file still exists\n if (out_dir / old_file).exists():\n if not force_all:\n # Rename to friendly filename if the old name was plain MPN\n if not dry_run:\n desc = existing.get(\"description\", \"\") or part.get(\"description\", \"\")\n mfg = existing.get(\"manufacturer\", \"\") or part.get(\"manufacturer\", \"\")\n new_file = friendly_filename(part_key, desc, mfg) + \".pdf\"\n if new_file != old_file and not (out_dir / new_file).exists():\n (out_dir / old_file).rename(out_dir / new_file)\n existing[\"file\"] = new_file\n print(f\" Renamed: {old_file} -> {new_file}\", file=sys.stderr)\n already_present.append(part_key)\n existing[\"references\"] = part[\"references\"]\n continue\n # File missing — re-download\n\n if status in (\"failed\", \"not_found\", \"no_datasheet\") and not (force or force_all):\n skipped_failed.append(part_key)\n continue\n\n to_download.append(part)\n\n if dry_run:\n summary = {\n \"would_download\": [p[\"_key\"] for p in to_download],\n \"already_present\": already_present,\n \"skipped_previous_failures\": skipped_failed,\n \"skipped_no_identifier\": skipped_no_id,\n }\n if as_json:\n json.dump(summary, sys.stdout, indent=2)\n else:\n print(f\"\\nDry run — would download {len(to_download)} datasheets:\")\n for p in to_download:\n print(f\" {p['_key']} ({p['manufacturer'] or 'unknown mfg'})\")\n print(f\"Already present: {len(already_present)}\")\n print(f\"Skipped (previous failures): {len(skipped_failed)}\")\n print(f\"Skipped (no identifier): {skipped_no_id}\")\n return summary\n\n if not to_download:\n msg = f\"All {len(already_present)} datasheets up to date.\"\n if skipped_failed:\n msg += f\" {len(skipped_failed)} previous failures (use --force to retry).\"\n print(msg, file=sys.stderr)\n # Still update the manifest (references may have changed)\n index[\"schematic\"] = str(mpn_list or input_path)\n index[\"last_sync\"] = datetime.now(timezone.utc).isoformat()\n save_index(index_path, index)\n return {\"downloaded\": 0, \"already_present\": len(already_present),\n \"failed\": len(skipped_failed)}\n\n # Get OAuth token\n auth = get_oauth_token()\n if auth is None:\n return {\"error\": \"Failed to authenticate with DigiKey API\"}\n token, client_id = auth\n\n # Process each part\n downloaded = []\n failed = []\n warnings = [] # verification warnings\n\n if parallel > 1:\n lock = threading.Lock()\n counter = [0]\n\n def _process_part(part):\n part_key = part[\"_key\"]\n with lock:\n counter[0] += 1\n n = counter[0]\n print(f\"[{n}/{len(to_download)}] {part_key}\", file=sys.stderr)\n\n result = sync_one_part(part, out_dir, token, client_id, index, delay)\n\n with lock:\n index.setdefault(\"parts\", {})[part_key] = result\n\n if result[\"status\"] == \"ok\":\n downloaded.append(part_key)\n vconf = result.get(\"verification\", \"\")\n vmark = \"\"\n if vconf == \"wrong\":\n vmark = \" ⚠ WRONG DATASHEET?\"\n warnings.append(part_key)\n elif vconf == \"unverified\":\n vmark = \" (unverified)\"\n print(f\" OK: {result['file']} ({result['size_bytes']:,} bytes){vmark}\",\n file=sys.stderr)\n else:\n failed.append(part_key)\n print(f\" {result['status'].upper()}: {result.get('error', '')}\",\n file=sys.stderr)\n\n index[\"schematic\"] = str(mpn_list or input_path)\n index[\"last_sync\"] = datetime.now(timezone.utc).isoformat()\n save_index(index_path, index)\n\n with ThreadPoolExecutor(max_workers=parallel) as executor:\n executor.map(_process_part, to_download)\n else:\n for i, part in enumerate(to_download):\n part_key = part[\"_key\"]\n print(f\"[{i+1}/{len(to_download)}] {part_key}\", file=sys.stderr)\n\n result = sync_one_part(part, out_dir, token, client_id, index, delay)\n\n # Handle token expiry — refresh once and retry\n if result.get(\"status\") == \"not_found\" and \"No DigiKey results\" in result.get(\"error\", \"\"):\n pass\n\n index.setdefault(\"parts\", {})[part_key] = result\n\n if result[\"status\"] == \"ok\":\n downloaded.append(part_key)\n vconf = result.get(\"verification\", \"\")\n vmark = \"\"\n if vconf == \"wrong\":\n vmark = \" ⚠ WRONG DATASHEET?\"\n warnings.append(part_key)\n elif vconf == \"unverified\":\n vmark = \" (unverified)\"\n print(f\" OK: {result['file']} ({result['size_bytes']:,} bytes){vmark}\",\n file=sys.stderr)\n else:\n failed.append(part_key)\n print(f\" {result['status'].upper()}: {result.get('error', '')}\",\n file=sys.stderr)\n\n index[\"schematic\"] = str(mpn_list or input_path)\n index[\"last_sync\"] = datetime.now(timezone.utc).isoformat()\n save_index(index_path, index)\n\n # Summary\n summary = {\n \"downloaded\": len(downloaded),\n \"already_present\": len(already_present),\n \"failed\": len(failed),\n \"verification_warnings\": len(warnings),\n \"skipped_previous_failures\": len(skipped_failed),\n \"skipped_no_identifier\": skipped_no_id,\n \"total_identified_parts\": len(parts),\n \"output_dir\": str(out_dir),\n \"index_path\": str(index_path),\n }\n\n if as_json:\n json.dump(summary, sys.stdout, indent=2)\n else:\n print(f\"\\nDatasheet sync complete:\", file=sys.stderr)\n print(f\" Downloaded: {len(downloaded)}\", file=sys.stderr)\n if downloaded:\n for m in downloaded:\n print(f\" {m}\", file=sys.stderr)\n if warnings:\n print(f\" Verification warnings: {len(warnings)}\", file=sys.stderr)\n for m in warnings:\n entry = index[\"parts\"].get(m, {})\n detail = entry.get(\"verification_details\", \"\")\n print(f\" {m}: {detail}\", file=sys.stderr)\n print(f\" Already present: {len(already_present)}\", file=sys.stderr)\n print(f\" Failed: {len(failed)}\", file=sys.stderr)\n if failed:\n for m in failed:\n entry = index[\"parts\"].get(m, {})\n url = entry.get(\"datasheet_url\", \"\")\n err = entry.get(\"error\", \"\")\n detail = f\" — {url}\" if url else f\" — {err}\"\n print(f\" {m}{detail}\", file=sys.stderr)\n if skipped_failed:\n print(f\" Skipped (previous failures, use --force): \"\n f\"{len(skipped_failed)}\", file=sys.stderr)\n print(f\" Skipped (no identifier): {skipped_no_id}\", file=sys.stderr)\n print(f\" Output: {out_dir}/\", file=sys.stderr)\n if failed:\n print(f\"\\n Tip: Try LCSC or element14 sync to fill gaps — they share\"\n f\" the same datasheets/ directory and skip already-downloaded parts.\",\n file=sys.stderr)\n if downloaded or already_present:\n print(f\"\\n Next: extract structured specs for IC-aware analyzer checks\",\n file=sys.stderr)\n print(f\" (VM-001, PS-001, PR-004, DP-002). The `datasheets` skill\",\n file=sys.stderr)\n print(f\" reads PDFs and writes {out_dir}/extracted/\u003cMPN>.json.\",\n file=sys.stderr)\n\n return summary\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Sync datasheets for a KiCad project via DigiKey API\",\n )\n input_group = parser.add_mutually_exclusive_group(required=True)\n input_group.add_argument(\n \"input\",\n nargs=\"?\",\n help=\"Path to .kicad_sch file or pre-computed analyzer JSON\",\n )\n input_group.add_argument(\n \"--mpn-list\",\n metavar=\"FILE\",\n help=(\"Path to a text file with one MPN per line (KH-312 batch mode). \"\n \"Skips blank lines and '#' comments. Output defaults to \"\n \"./datasheets/ in cwd when --output is not given.\"),\n )\n parser.add_argument(\n \"--output\", \"-o\",\n help=(\"Output directory (default: datasheets/ next to input, \"\n \"or ./datasheets/ in cwd when --mpn-list is used)\"),\n )\n parser.add_argument(\n \"--force\", action=\"store_true\",\n help=\"Retry previously failed downloads\",\n )\n parser.add_argument(\n \"--force-all\", action=\"store_true\",\n help=\"Re-download everything, including already-present files\",\n )\n parser.add_argument(\n \"--delay\", type=float, default=1.0,\n help=\"Seconds between DigiKey API calls (default: 1.0)\",\n )\n parser.add_argument(\n \"--parallel\", type=int, default=1,\n help=\"Number of parallel download workers (default: 1)\",\n )\n parser.add_argument(\n \"--dry-run\", action=\"store_true\",\n help=\"Show what would be downloaded without doing it\",\n )\n parser.add_argument(\n \"--json\", action=\"store_true\",\n help=\"Output summary as JSON\",\n )\n args = parser.parse_args()\n\n result = sync_datasheets(\n input_path=args.input,\n output_dir=args.output,\n force=args.force,\n force_all=args.force_all,\n delay=args.delay,\n parallel=args.parallel,\n dry_run=args.dry_run,\n as_json=args.json,\n mpn_list=args.mpn_list,\n )\n\n if \"error\" in result:\n sys.exit(1)\n if result.get(\"failed\", 0) > 0:\n sys.exit(0) # Partial success is still success\n sys.exit(0)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":34777,"content_sha256":"87a7751015468a5944c25b0e1de778d86d582319a69469f74460d86568a64843"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"DigiKey Parts Search & Analysis","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skill","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"kicad","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Schematic analysis — extracts MPNs for datasheet sync","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bom","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"BOM management — orchestrates sourcing across distributors","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"spice","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Uses DigiKey parametric data for behavioral SPICE models","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"DigiKey is the ","type":"text"},{"text":"primary source for prototype orders","type":"text","marks":[{"type":"strong"}]},{"text":" (Mouser is secondary). Its API returns direct PDF datasheet links, making it the preferred datasheet source. For production orders, see ","type":"text"},{"text":"lcsc","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"jlcpcb","type":"text","marks":[{"type":"code_inline"}]},{"text":". For BOM management and export workflows, see ","type":"text"},{"text":"bom","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"API Credential Setup","type":"text"}]},{"type":"paragraph","content":[{"text":"The DigiKey API requires OAuth 2.0 credentials. Here's how to set them up:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create a DigiKey account","type":"text","marks":[{"type":"strong"}]},{"text":" at ","type":"text"},{"text":"digikey.com","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.digikey.com","title":null}}]},{"text":" if you don't have one","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Register an API app","type":"text","marks":[{"type":"strong"}]},{"text":" at ","type":"text"},{"text":"developer.digikey.com","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.digikey.com","title":null}}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sign in with your DigiKey account","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go to \"My Apps\" → \"Create App\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"App name: anything (e.g., \"kicad-happy\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Select ","type":"text"},{"text":"\"Product Information v4\"","type":"text","marks":[{"type":"strong"}]},{"text":" API","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OAuth type: ","type":"text"},{"text":"Client Credentials","type":"text","marks":[{"type":"strong"}]},{"text":" (2-legged, no user login needed)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Callback URL: ","type":"text"},{"text":"https://localhost","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not used for client credentials, but required)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After creation, note the ","type":"text"},{"text":"Client ID","type":"text","marks":[{"type":"strong"}]},{"text":" and ","type":"text"},{"text":"Client Secret","type":"text","marks":[{"type":"strong"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set the environment variables","type":"text","marks":[{"type":"strong"}]},{"text":" before running the scripts:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"export DIGIKEY_CLIENT_ID=your_client_id_here\nexport DIGIKEY_CLIENT_SECRET=your_client_secret_here","type":"text"}]},{"type":"paragraph","content":[{"text":"If credentials are stored in a central secrets file (e.g., ","type":"text"},{"text":"~/.config/secrets.env","type":"text","marks":[{"type":"code_inline"}]},{"text":"), load them first:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"export $(grep -v '^#' ~/.config/secrets.env | grep -v '^

DigiKey Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for datasheet sync | | | BOM management — orchestrates sourcing across distributors | | | Uses DigiKey parametric data for behavioral SPICE models | DigiKey is the primary source for prototype orders (Mouser is secondary). Its API returns direct PDF datasheet links, making it the preferred datasheet source. For production orders, see / . For BOM management and export workflows, see . API Credential Setup The DigiKey API requires OAuth 2.0 credentials. Here's how to set…

| xargs)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The client credentials flow has no user interaction — once configured, API calls work automatically.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"DigiKey Product Information API v4","type":"text"}]},{"type":"paragraph","content":[{"text":"The API is the preferred way to search DigiKey. It returns structured JSON with full product details, pricing, stock, datasheets, and parametric data.","type":"text"}]},{"type":"paragraph","content":[{"text":"Base URL:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"https://api.digikey.com","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Authentication","type":"text"}]},{"type":"paragraph","content":[{"text":"All API requests require OAuth 2.0. Use the ","type":"text"},{"text":"client credentials flow","type":"text","marks":[{"type":"strong"}]},{"text":" (2-legged). Credentials must be loaded as environment variables (see \"API Credential Setup\" above).","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -s -X POST https://api.digikey.com/v1/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"client_id=${DIGIKEY_CLIENT_ID}&client_secret=${DIGIKEY_CLIENT_SECRET}&grant_type=client_credentials\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The response returns an ","type":"text"},{"text":"access_token","type":"text","marks":[{"type":"code_inline"}]},{"text":" valid for ","type":"text"},{"text":"10 minutes","type":"text","marks":[{"type":"strong"}]},{"text":". Cache the token in a shell variable and reuse it for subsequent calls in the same session. If you get a 401 error mid-session, the token has expired — re-authenticate to get a fresh one.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required Headers","type":"text"}]},{"type":"paragraph","content":[{"text":"Every API call needs:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"X-DIGIKEY-Client-Id: ${DIGIKEY_CLIENT_ID}\nAuthorization: Bearer \u003caccess_token>","type":"text"}]},{"type":"paragraph","content":[{"text":"Optional locale headers:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"X-DIGIKEY-Locale-Language","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default), ","type":"text"},{"text":"ja","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"de","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"fr","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ko","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"zhs","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"zht","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"it","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"es","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"X-DIGIKEY-Locale-Currency","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"USD","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default), ","type":"text"},{"text":"CAD","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"EUR","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"GBP","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"JPY","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"X-DIGIKEY-Locale-Site","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"US","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default), ","type":"text"},{"text":"CA","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"UK","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"DE","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"KeywordSearch — Find Parts","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /products/v4/search/keyword","type":"text"}]},{"type":"paragraph","content":[{"text":"This is the primary search endpoint. Search by MPN, DigiKey part number, description, or keywords.","type":"text"}]},{"type":"paragraph","content":[{"text":"Request body:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"Keywords\": \"GRM155R71C104KA88D\",\n \"Limit\": 25,\n \"Offset\": 0,\n \"FilterOptionsRequest\": {\n \"MinimumQuantityAvailable\": 1,\n \"SearchOptions\": [\"InStock\", \"HasDatasheet\", \"RoHSCompliant\"],\n \"ManufacturerFilter\": [{\"Id\": \"...\"}],\n \"CategoryFilter\": [{\"Id\": \"...\"}],\n \"StatusFilter\": [{\"Id\": \"...\"}],\n \"MarketPlaceFilter\": \"ExcludeMarketPlace\"\n },\n \"SortOptions\": {\n \"Field\": \"Price\",\n \"SortOrder\": \"Ascending\"\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Key request fields:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keywords","type":"text","marks":[{"type":"code_inline"}]},{"text":" (string, max 250 chars) — search term (MPN, DK PN, description)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Limit","type":"text","marks":[{"type":"code_inline"}]},{"text":" (int, 1-50) — results per page","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Offset","type":"text","marks":[{"type":"code_inline"}]},{"text":" (int) — pagination offset","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SearchOptions","type":"text","marks":[{"type":"code_inline"}]},{"text":" — array of: ","type":"text"},{"text":"InStock","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"HasDatasheet","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"RoHSCompliant","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"NormallyStocking","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Has3DModel","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"HasCadModel","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"HasProductPhoto","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"NewProduct","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SortOptions.Field","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"Price","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"QuantityAvailable","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Manufacturer","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ManufacturerProductNumber","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"DigiKeyProductNumber","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"MinimumQuantity","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MarketPlaceFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"NoFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ExcludeMarketPlace","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"MarketPlaceOnly","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Response — key fields in each ","type":"text"},{"text":"Products[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" item:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"ManufacturerProductNumber\": \"GRM155R71C104KA88D\",\n \"Manufacturer\": {\"Id\": 563, \"Name\": \"Murata Electronics\"},\n \"Description\": {\n \"ProductDescription\": \"CAP CER 100NF 16V X7R 0402\",\n \"DetailedDescription\": \"...\"\n },\n \"UnitPrice\": 0.01,\n \"QuantityAvailable\": 248000,\n \"ProductUrl\": \"https://www.digikey.com/...\",\n \"DatasheetUrl\": \"https://...\",\n \"PhotoUrl\": \"https://...\",\n \"ProductVariations\": [\n {\n \"DigiKeyProductNumber\": \"490-10698-1-ND\",\n \"PackageType\": {\"Name\": \"Cut Tape\"},\n \"StandardPricing\": [\n {\"BreakQuantity\": 1, \"UnitPrice\": 0.01, \"TotalPrice\": 0.01},\n {\"BreakQuantity\": 10, \"UnitPrice\": 0.008, \"TotalPrice\": 0.08}\n ],\n \"QuantityAvailableforPackageType\": 248000,\n \"MinimumOrderQuantity\": 1,\n \"StandardPackage\": 10000\n }\n ],\n \"Parameters\": [\n {\"ParameterText\": \"Capacitance\", \"ValueText\": \"100nF\"},\n {\"ParameterText\": \"Voltage Rated\", \"ValueText\": \"16V\"},\n {\"ParameterText\": \"Temperature Coefficient\", \"ValueText\": \"X7R\"},\n {\"ParameterText\": \"Package / Case\", \"ValueText\": \"0402 (1005 Metric)\"}\n ],\n \"ProductStatus\": {\"Status\": \"Active\"},\n \"Category\": {\"Name\": \"Ceramic Capacitors\"},\n \"Classifications\": {\"RohsStatus\": \"ROHS3 Compliant\"},\n \"Discontinued\": false,\n \"EndOfLife\": false,\n \"NormallyStocking\": true\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"ProductDetails — Full Details for One Part","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"GET /products/v4/search/{productNumber}/productdetails","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this for expanded information on a specific part. ","type":"text"},{"text":"{productNumber}","type":"text","marks":[{"type":"code_inline"}]},{"text":" can be a DigiKey part number or manufacturer part number.","type":"text"}]},{"type":"paragraph","content":[{"text":"Query parameters:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"manufacturerId","type":"text","marks":[{"type":"code_inline"}]},{"text":" (optional) — disambiguate MPNs that match multiple manufacturers (e.g., \"CR2032\")","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Returns the full ","type":"text"},{"text":"Product","type":"text","marks":[{"type":"code_inline"}]},{"text":" object with all parameters, pricing (including MyPricing if authenticated with account), media links, and related products.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Other Useful Endpoints","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Endpoint","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Method","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/{pn}/productdetails","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full product info for one part","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/productpricing/{pn}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pricing with MyPricing for a part","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/{pn}/media","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All media (images, datasheets) for a part","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/manufacturers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All manufacturers (use IDs in KeywordSearch filters)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/categories","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All categories (use IDs in KeywordSearch filters)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/{pn}/alternatepackaging","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Alternate packaging options","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/{pn}/substitutions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Substitute parts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/products/v4/search/{pn}/recommendedproducts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GET","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recommended/associated parts","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rate Limits","type":"text"}]},{"type":"paragraph","content":[{"text":"Per-minute and daily quotas apply. HTTP 429 with ","type":"text"},{"text":"Retry-After","type":"text","marks":[{"type":"code_inline"}]},{"text":" header on exceed.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Error Responses","type":"text"}]},{"type":"paragraph","content":[{"text":"All errors return ","type":"text"},{"text":"DKProblemDetails","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\"type\": \"...\", \"title\": \"...\", \"status\": 401, \"detail\": \"Invalid token\", \"correlationId\": \"...\"}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fallback: Fetch DigiKey Website","type":"text"}]},{"type":"paragraph","content":[{"text":"If API credentials are not available or authentication fails, search DigiKey by fetching product pages directly:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"https://www.digikey.com/en/products/result?keywords=\u003curl-encoded-query>","type":"text"}]},{"type":"paragraph","content":[{"text":"Examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"https://www.digikey.com/en/products/result?keywords=GRM155R71C104KA88D","type":"text","marks":[{"type":"code_inline"}]},{"text":" (by MPN)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"https://www.digikey.com/en/products/result?keywords=100nF+0402+X7R+16V","type":"text","marks":[{"type":"code_inline"}]},{"text":" (by specs)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Results from DigiKey can be noisy (JS-heavy pages). Look for the product table rows containing: DigiKey part number, MPN, description, unit price, stock quantity, and datasheet links. If results are truncated or empty, try searching by exact MPN rather than keywords.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Datasheet Download & Analysis","type":"text"}]},{"type":"paragraph","content":[{"text":"DigiKey's API provides ","type":"text"},{"text":"direct PDF URLs","type":"text","marks":[{"type":"strong"}]},{"text":" for datasheets — this is the preferred method for downloading datasheets because it avoids web scraping and returns reliable, stable links. Other skills (kicad, bom) should use DigiKey API as the first-choice datasheet source.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Datasheet Directory Sync (Primary Workflow)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"sync_datasheets_digikey.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" to maintain a ","type":"text"},{"text":"datasheets/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory alongside a KiCad project. It extracts components from the schematic, searches DigiKey for datasheet URLs, downloads missing PDFs, and writes an ","type":"text"},{"text":"manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" manifest. Subsequent runs are incremental — only new or changed parts are fetched.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Sync datasheets for a KiCad project (creates datasheets/ next to the schematic)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch>\n\n# Preview what would be downloaded\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --dry-run\n\n# Retry previously failed downloads\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --force\n\n# Custom output directory\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> -o ./my-datasheets\n\n# Use pre-computed analyzer JSON instead of running the analyzer\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py analyzer_output.json\n\n# Parallel downloads (3 workers)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py \u003cfile.kicad_sch> --parallel 3\n\n# Batch mode — sync from a plain MPN list (no KiCad project required)\npython3 \u003cskill-path>/scripts/sync_datasheets_digikey.py --mpn-list mpns.txt --output ./datasheets","type":"text"}]},{"type":"paragraph","content":[{"text":"MPN-list batch mode","type":"text","marks":[{"type":"strong"}]},{"text":" (KH-312) — when you have a list of MPNs but no KiCad project to point at (harness datasheet seeding, bulk seeding a new part library). The file format is one MPN per line. Blank lines and ","type":"text"},{"text":"#","type":"text","marks":[{"type":"code_inline"}]},{"text":" comments (full-line and inline) are skipped. Non-MPN strings (generic values like ","type":"text"},{"text":"100nF","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"DNP","type":"text","marks":[{"type":"code_inline"}]},{"text":") are filtered via ","type":"text"},{"text":"is_real_mpn()","type":"text","marks":[{"type":"code_inline"}]},{"text":" and de-duplicated. Output defaults to ","type":"text"},{"text":"./datasheets/","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the current working directory when ","type":"text"},{"text":"--output","type":"text","marks":[{"type":"code_inline"}]},{"text":" is omitted.","type":"text"}]},{"type":"paragraph","content":[{"text":"The script:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Runs the kicad schematic analyzer","type":"text","marks":[{"type":"strong"}]},{"text":" automatically to extract components and MPNs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Filters generic passives","type":"text","marks":[{"type":"strong"}]},{"text":" — skips entries without real MPNs (e.g., \"100nF\", \"10K\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tries schematic URLs first","type":"text","marks":[{"type":"strong"}]},{"text":" — uses the datasheet URL embedded in the KiCad symbol before hitting the DigiKey API, saving API calls","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Writes ","type":"text","marks":[{"type":"strong"}]},{"text":"manifest.json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" manifest","type":"text","marks":[{"type":"strong"}]},{"text":" — maps each MPN to its PDF file, manufacturer, description, download status, and URL. The kicad skill reads this during design review to cross-reference datasheets with the schematic.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tracks failures","type":"text","marks":[{"type":"strong"}]},{"text":" — failed downloads are recorded with error details and not retried on subsequent runs unless ","type":"text"},{"text":"--force","type":"text","marks":[{"type":"code_inline"}]},{"text":" is used","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rate-limited","type":"text","marks":[{"type":"strong"}]},{"text":" — 1 second between DigiKey API calls (configurable with ","type":"text"},{"text":"--delay","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Saves progress incrementally","type":"text","marks":[{"type":"strong"}]},{"text":" — if interrupted, already-downloaded files are preserved","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" manifest structure:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"schematic\": \"/path/to/file.kicad_sch\",\n \"last_sync\": \"2026-03-09T04:44:30+00:00\",\n \"parts\": {\n \"TPS61023DRLR\": {\n \"file\": \"TPS61023DRLR.pdf\",\n \"manufacturer\": \"Texas Instruments\",\n \"description\": \"Boost converter\",\n \"datasheet_url\": \"https://...\",\n \"status\": \"ok\",\n \"references\": [\"U3\", \"U2\"],\n \"size_bytes\": 2392725\n }\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Single Datasheet Download","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"fetch_datasheet_digikey.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" for one-off datasheet downloads. It handles manufacturer-specific quirks automatically.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Search by MPN (uses DigiKey API, requires credentials)\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py --search \"TPS61023\" -o datasheet.pdf\n\n# Direct URL download\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py \"https://www.ti.com/lit/gpn/tps61023\" -o datasheet.pdf\n\n# JSON output for script integration\npython3 \u003cskill-path>/scripts/fetch_datasheet_digikey.py --search \"ADP1706\" --json","type":"text"}]},{"type":"paragraph","content":[{"text":"The script:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OS-agnostic","type":"text","marks":[{"type":"strong"}]},{"text":" — uses Python ","type":"text"},{"text":"requests","type":"text","marks":[{"type":"code_inline"}]},{"text":" library (no wget/curl dependency). Falls back to ","type":"text"},{"text":"urllib","type":"text","marks":[{"type":"code_inline"}]},{"text":" if ","type":"text"},{"text":"requests","type":"text","marks":[{"type":"code_inline"}]},{"text":" isn't installed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Normalizes redirect URLs","type":"text","marks":[{"type":"strong"}]},{"text":" — DigiKey's ","type":"text"},{"text":"DatasheetUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" for TI parts points to a JS redirect page; the script extracts the direct PDF link. Also fixes protocol-relative ","type":"text"},{"text":"//mm.digikey.com/...","type":"text","marks":[{"type":"code_inline"}]},{"text":" URLs.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sets proper User-Agent","type":"text","marks":[{"type":"strong"}]},{"text":" — many manufacturer sites (Nexperia, Lite-On, STMicro, Molex) block bare ","type":"text"},{"text":"urllib","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"curl","type":"text","marks":[{"type":"code_inline"}]},{"text":" requests but serve PDFs fine with a browser User-Agent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validates PDF headers","type":"text","marks":[{"type":"strong"}]},{"text":" — rejects HTML error pages or Cloudflare challenge pages that masquerade as downloads","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Falls back to alternative sources","type":"text","marks":[{"type":"strong"}]},{"text":" — tries known URL patterns for Microchip when the primary URL fails","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Headless browser fallback","type":"text","marks":[{"type":"strong"}]},{"text":" — if ","type":"text"},{"text":"playwright","type":"text","marks":[{"type":"code_inline"}]},{"text":" is installed, automatically uses a headless Chromium browser as a last resort for sites that serve PDFs via JavaScript (Broadcom doc viewer, Espressif download redirects). Intercepts download events and reads response bodies directly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exit codes","type":"text","marks":[{"type":"strong"}]},{"text":": 0 = success, 1 = download failed, 2 = search/API error","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dependencies","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pip install requests","type":"text","marks":[{"type":"code_inline"}]},{"text":" (strongly recommended; urllib fallback can't handle HTTP/2 sites like analog.com)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pip install playwright && playwright install chromium","type":"text","marks":[{"type":"code_inline"}]},{"text":" (optional; enables headless browser fallback for JS-heavy sites)","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Manufacturer Compatibility","type":"text"}]},{"type":"paragraph","content":[{"text":"Tested against 240 components across 8 open-source KiCad projects (96% download success rate, 94% without Playwright):","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manufacturer","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URL normalization strips JS redirect wrapper","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADI / Analog","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"requests","type":"text","marks":[{"type":"code_inline"}]},{"text":" handles HTTP/2 transparently","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"STMicro","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires User-Agent header","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Nexperia","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires User-Agent header","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lite-On","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires User-Agent header","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Molex","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires User-Agent header","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Renesas","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ON Semi","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NXP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Diodes Inc","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Microchip","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct download via API URLs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"YAGEO, Samsung, Murata","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DigiKey-hosted PDFs (","type":"text"},{"text":"mm.digikey.com","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Broadcom","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works*","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires Playwright — ","type":"text"},{"text":"docs.broadcom.com","type":"text","marks":[{"type":"code_inline"}]},{"text":" serves PDFs via JS download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Espressif","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Works*","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Requires Playwright — download redirect needs JS execution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lattice","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mixed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Some URLs require cookies/auth","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"* Requires ","type":"text"},{"text":"playwright","type":"text","marks":[{"type":"code_inline"}]},{"text":" package — falls back gracefully to user notification if not installed.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When Download Fails","type":"text"}]},{"type":"paragraph","content":[{"text":"If the script or inline download fails (exit code 1), ","type":"text"},{"text":"tell the user and provide the URL","type":"text","marks":[{"type":"strong"}]},{"text":" so they can open it in a real browser. Some manufacturer sites (Lattice, TDK InvenSense) require interactive login, cookies, or CAPTCHA that even a headless browser can't handle. With Playwright installed, Broadcom and Espressif now download automatically.","type":"text"}]},{"type":"paragraph","content":[{"text":"Example message to the user:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"I couldn't download the datasheet for ICE40UP5K-SG48I automatically — Lattice's site requires browser authentication. Here's the direct link: https://www.latticesemi.com/-/media/LatticeSemi/Documents/DataSheets/iCE/FPGA-DS-02008-1-9-iCE40-Ultra-Plus-Family-Data-Sheet.ashx","type":"text"}]},{"type":"paragraph","content":[{"text":"You can open it in your browser and save it locally, then I can read and analyze it.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" output always includes the ","type":"text"},{"text":"datasheet_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" field even on failure, so you can extract the URL programmatically.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Manual Download Workflow","type":"text"}]},{"type":"paragraph","content":[{"text":"If the script isn't available or you need to do it inline:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search for the part","type":"text","marks":[{"type":"strong"}]},{"text":" using KeywordSearch or ProductDetails","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Extract ","type":"text","marks":[{"type":"strong"}]},{"text":"DatasheetUrl","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" from the API response","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Normalize the URL","type":"text","marks":[{"type":"strong"}]},{"text":" — if it starts with ","type":"text"},{"text":"//","type":"text","marks":[{"type":"code_inline"}]},{"text":", prepend ","type":"text"},{"text":"https:","type":"text","marks":[{"type":"code_inline"}]},{"text":". If it contains ","type":"text"},{"text":"ti.com/general/docs/suppproductinfo","type":"text","marks":[{"type":"code_inline"}]},{"text":", extract the ","type":"text"},{"text":"gotoUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" query parameter.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download with ","type":"text","marks":[{"type":"strong"}]},{"text":"requests","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (Python) or ","type":"text"},{"text":"wget","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"curl","type":"text","marks":[{"type":"code_inline"}]},{"text":" with a browser User-Agent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify it's a PDF","type":"text","marks":[{"type":"strong"}]},{"text":": first 4 bytes should be ","type":"text"},{"text":"%PDF","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"If the ","type":"text"},{"text":"DatasheetUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" field is empty or all download methods fail:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provide the URL to the user for manual browser download","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Try the ","type":"text"},{"text":"/products/v4/search/{pn}/media","type":"text","marks":[{"type":"code_inline"}]},{"text":" endpoint for alternative media links","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Web search as a last resort: ","type":"text"},{"text":"\"\u003cMPN> datasheet filetype:pdf\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"What to Extract from Datasheets","type":"text"}]},{"type":"paragraph","content":[{"text":"When analyzing a datasheet for a KiCad design review (see ","type":"text"},{"text":"kicad","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Absolute maximum ratings","type":"text","marks":[{"type":"strong"}]},{"text":" — voltage, current, temperature limits","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recommended operating conditions","type":"text","marks":[{"type":"strong"}]},{"text":" — typical operating ranges","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pinout and pin descriptions","type":"text","marks":[{"type":"strong"}]},{"text":" — verify against KiCad symbol","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Package dimensions","type":"text","marks":[{"type":"strong"}]},{"text":" — verify against KiCad footprint","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Typical application circuit","type":"text","marks":[{"type":"strong"}]},{"text":" — compare against the user's schematic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Thermal characteristics","type":"text","marks":[{"type":"strong"}]},{"text":" — θJA, θJC for power dissipation calculations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Electrical characteristics","type":"text","marks":[{"type":"strong"}]},{"text":" — key parameters (Vout, Iq, PSRR, etc.)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tips","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DigiKey PN suffixes: ","type":"text"},{"text":"-ND","type":"text","marks":[{"type":"code_inline"}]},{"text":" standard, ","type":"text"},{"text":"-1-ND","type":"text","marks":[{"type":"code_inline"}]},{"text":" cut tape, ","type":"text"},{"text":"-2-ND","type":"text","marks":[{"type":"code_inline"}]},{"text":" digi-reel, ","type":"text"},{"text":"-6-ND","type":"text","marks":[{"type":"code_inline"}]},{"text":" full reel","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"ExcludeMarketPlace","type":"text","marks":[{"type":"code_inline"}]},{"text":" filter to avoid third-party seller listings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Price breaks in ","type":"text"},{"text":"ProductVariations[].StandardPricing[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" — check ","type":"text"},{"text":"BreakQuantity","type":"text","marks":[{"type":"code_inline"}]},{"text":" thresholds","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check ","type":"text"},{"text":"ProductStatus","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"Discontinued","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"EndOfLife","type":"text","marks":[{"type":"code_inline"}]},{"text":" before selecting parts","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"digikey","author":"@skillopedia","source":{"stars":464,"repo_name":"kicad-happy","origin_url":"https://github.com/aklofas/kicad-happy/blob/HEAD/skills/digikey/SKILL.md","repo_owner":"aklofas","body_sha256":"f60bb11c40fbea747bca48ca370269d774ed9fc5ed8ae3f692b42960c4825a0d","cluster_key":"9a7194b2905e2871711ab2340a75e2871bc14b4db85bd03704e7ec1652a1ddb1","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aklofas/kicad-happy/skills/digikey/SKILL.md","attachments":[{"id":"f793374d-18e6-5316-b818-8d44d7b55e25","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f793374d-18e6-5316-b818-8d44d7b55e25/attachment.py","path":"scripts/fetch_datasheet_digikey.py","size":20141,"sha256":"ce4f4756f6883696d1c1f7daaaf8cdc59751d0d7a194b15d44ce2a9c8baf58f0","contentType":"text/x-python; charset=utf-8"},{"id":"2b5565c8-babb-5432-bbfa-8782939036ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2b5565c8-babb-5432-bbfa-8782939036ac/attachment.py","path":"scripts/sync_datasheets_digikey.py","size":34777,"sha256":"87a7751015468a5944c25b0e1de778d86d582319a69469f74460d86568a64843","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"bccce69b1bc2a42ada3f9dae7a778596bfbdcb53ea183be9c2ece923c9cce26d","attachment_count":2,"text_attachments":2,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/digikey/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Search DigiKey for electronic components and download datasheets — primary source for prototype orders and the preferred API method for fetching datasheets. Find parts by keyword or MPN, check pricing/stock, download datasheets via API, analyze specifications. Sync and maintain a local datasheets directory — extract components from schematics, download missing datasheets, keep them up to date. Also supports batch MPN-list seeding (`--mpn-list`) for bulk workflows without a KiCad project. Use when the user asks about electronic components, part specs, datasheets, pricing, stock, footprints, or needs to download a datasheet — even without mentioning \"DigiKey\". Also for \"sync datasheets\", \"download datasheets for my board/project\", or mentions a datasheets directory. DigiKey is the default distributor for prototyping. For BOM workflows, see the bom skill."}},"renderedAt":1782980376795}

DigiKey Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for datasheet sync | | | BOM management — orchestrates sourcing across distributors | | | Uses DigiKey parametric data for behavioral SPICE models | DigiKey is the primary source for prototype orders (Mouser is secondary). Its API returns direct PDF datasheet links, making it the preferred datasheet source. For production orders, see / . For BOM management and export workflows, see . API Credential Setup The DigiKey API requires OAuth 2.0 credentials. Here's how to set…