Mouser Electronics Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for part lookup | | | BOM management — orchestrates sourcing across distributors | | | Primary prototype source (prefer for datasheets — direct PDF links) | | | Uses Mouser parametric data for behavioral SPICE models | Mouser is the secondary source for prototype orders — use when DigiKey is out of stock or has worse pricing. For production orders, see / . For BOM management and export workflows, see . For datasheets, prefer DigiKey's API (direct PDF links)…

| xargs)\n```\n\n## Mouser Search API Reference\n\nAll search endpoints use the Search API key as a query parameter: `?apiKey=\u003ckey>`. Content-Type is `application/json` for all POST requests.\n\n### V1 Endpoints\n\n#### Keyword Search\n```\nPOST /api/v1/search/keyword?apiKey=\u003ckey>\n```\n```json\n{\n \"SearchByKeywordRequest\": {\n \"keyword\": \"100nF 0402 ceramic capacitor\",\n \"records\": 50,\n \"startingRecord\": 0,\n \"searchOptions\": \"InStock\"\n }\n}\n```\n- `searchOptions`: `\"None\"` | `\"Rohs\"` | `\"InStock\"` | `\"RohsAndInStock\"`\n- `records`: max 50 per request\n- `startingRecord`: offset for pagination\n\n#### Part Number Search\n```\nPOST /api/v1/search/partnumber?apiKey=\u003ckey>\n```\n```json\n{\n \"SearchByPartRequest\": {\n \"mouserPartNumber\": \"GRM155R71C104KA88D|RC0402FR-0710KL\",\n \"partSearchOptions\": \"Exact\"\n }\n}\n```\n- Up to **10 part numbers**, pipe-separated (`|`)\n- Works with both Mouser part numbers AND manufacturer part numbers (MPNs)\n- `partSearchOptions`: `\"Exact\"` | `\"BeginsWith\"` | `\"Contains\"`\n\n### V2 Endpoints\n\nV2 adds manufacturer filtering and pagination by page number.\n\n#### Keyword + Manufacturer Search\n```\nPOST /api/v2/search/keywordandmanufacturer?apiKey=\u003ckey>\n```\n```json\n{\n \"SearchByKeywordMfrNameRequest\": {\n \"keyword\": \"LMR51450\",\n \"manufacturerName\": \"Texas Instruments\",\n \"records\": 25,\n \"pageNumber\": 1,\n \"searchOptions\": \"InStock\"\n }\n}\n```\nNote: the wrapper object name is `SearchByKeywordMfrNameRequest` (not `SearchByKeywordMfrRequest` — the V1 name is deprecated).\n\n#### Part Number + Manufacturer Search\n```\nPOST /api/v2/search/partnumberandmanufacturer?apiKey=\u003ckey>\n```\n\n#### Manufacturer List\n```\nGET /api/v2/search/manufacturerlist?apiKey=\u003ckey>\n```\nReturns the full list of manufacturer names for use in filtered searches.\n\n### V1 Deprecated Endpoints\n\nThese still work but V2 equivalents are preferred:\n- `POST /api/v1/search/keywordandmanufacturer` → use V2\n- `POST /api/v1/search/partnumberandmanufacturer` → use V2\n- `GET /api/v1/search/manufacturerlist` → use V2\n\n## Search Response Structure\n\nAll search endpoints return the same response format:\n\n```json\n{\n \"Errors\": [],\n \"SearchResults\": {\n \"NumberOfResult\": 142,\n \"Parts\": [...]\n }\n}\n```\n\n### Key Part Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `MouserPartNumber` | string | Mouser's internal part number (prefixed, e.g., `81-GRM155R71C104KA88`) |\n| `ManufacturerPartNumber` | string | Manufacturer's part number (MPN) — use for cross-distributor matching |\n| `Manufacturer` | string | Manufacturer name |\n| `Description` | string | Product description |\n| `Category` | string | Product category |\n| `DataSheetUrl` | string | URL to datasheet PDF (Mouser-hosted — see Datasheet section) |\n| `ProductDetailUrl` | string | URL to Mouser product page |\n| `ImagePath` | string | Product image URL |\n| `Availability` | string | Human-readable stock text (e.g., \"2648712 In Stock\") |\n| `AvailabilityInStock` | string | Numeric stock quantity (as string) |\n| `AvailabilityOnOrder` | array | Incoming stock: `[{Quantity, Date}]` |\n| `LeadTime` | string | Factory lead time (e.g., \"84 Days\") |\n| `LifecycleStatus` | string\\|null | \"New Product\", \"End of Life\", etc. |\n| `IsDiscontinued` | string | \"true\" or \"false\" (string, not boolean) |\n| `SuggestedReplacement` | string | Replacement MPN if discontinued |\n| `Min` | string | Minimum order quantity (as string) |\n| `Mult` | string | Order multiple (as string) |\n| `Reeling` | bool | Tape-and-reel packaging available |\n| `ROHSStatus` | string | RoHS compliance status |\n| `PriceBreaks` | array | Tiered pricing: `[{Quantity, Price, Currency}]` |\n| `ProductAttributes` | array | Parametric specs: `[{AttributeName, AttributeValue}]` |\n| `AlternatePackagings` | array\\|null | Alternate packaging MPNs: `[{APMfrPN}]` |\n| `SurchargeMessages` | array | Tariff/surcharge info: `[{code, message}]` |\n| `ProductCompliance` | array | HTS codes, ECCN: `[{ComplianceName, ComplianceValue}]` |\n| `UnitWeightKg` | object | `{UnitWeight: \u003cfloat>}` in kg |\n\n### Quirks and Gotchas\n\n- **Price is a string** with currency symbol: `\"$0.10\"`, not a float. Parse it before comparing.\n- **Stock is a string**: `AvailabilityInStock` returns `\"2648712\"` not `2648712`.\n- **IsDiscontinued is a string**: `\"true\"` or `\"false\"`, not boolean.\n- **Mouser part numbers have prefixes**: `81-GRM155R71C104KA88` — the prefix is Mouser-specific. Use `ManufacturerPartNumber` for cross-referencing.\n- **V2 wrapper names differ from V1**: V2 uses `SearchByKeywordMfrNameRequest`, not `SearchByKeywordMfrRequest`.\n- **Tariff info**: `SurchargeMessages` may contain US tariff percentages — useful for cost estimation.\n- **On-order data**: `AvailabilityOnOrder` shows incoming stock quantities and expected dates.\n\n## Datasheet Download & Sync\n\nMouser's `DataSheetUrl` field points to Mouser-hosted URLs (`mouser.com/datasheet/...`). These URLs **block automated downloads** — they return an HTML \"Access denied\" page when fetched with Python/curl/wget, even with browser User-Agent headers. The download scripts handle this with a multi-strategy approach:\n\n1. Try the Mouser datasheet URL directly (works for some parts)\n2. Scrape the Mouser product page HTML for alternative datasheet links\n3. Try manufacturer-specific alternative URL patterns\n\nNote: Mouser's product pages return 403 for most automated requests, so strategy 2 has limited success. DigiKey and LCSC are more reliable datasheet sources.\n\n### Datasheet Directory Sync\n\nUse `sync_datasheets_mouser.py` to maintain a `datasheets/` directory alongside a KiCad project. Same workflow and `manifest.json` format as the DigiKey skill.\n\n```bash\n# Sync datasheets for a KiCad project\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch>\n\n# Preview what would be downloaded\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> --dry-run\n\n# Retry previously failed downloads\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> --force\n\n# Custom output directory\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> -o ./my-datasheets\n\n# Parallel downloads (3 workers)\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.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_mouser.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\nKiCad project to point at (harness datasheet seeding, bulk part-library\nseeding). One MPN per line; blank lines and `#` comments (full-line and\ninline) are skipped; generic values (`100nF`, `DNP`) are filtered via\n`is_real_mpn()` and de-duplicated. Output defaults to `./datasheets/` in\nthe current working directory when `--output` is omitted.\n\n### Single Datasheet Download\n\nUse `fetch_datasheet_mouser.py` for one-off downloads.\n\n```bash\n# Search by MPN (uses Mouser API)\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py --search \"TPS61023DRLR\" -o datasheet.pdf\n\n# Direct URL download\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py \"https://example.com/datasheet.pdf\" -o datasheet.pdf\n\n# JSON output\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py --search \"ADP1706\" --json\n```\n\n### Download Strategy\n\nThe scripts try multiple sources in order:\n1. **Schematic URL** — uses the datasheet URL embedded in the KiCad symbol (sync only)\n2. **Mouser API search** — gets the `DataSheetUrl` from the API response\n3. **Alternative manufacturer sources** — tries known URL patterns for major manufacturers (TI, Microchip, etc.) when the Mouser URL is blocked\n4. **Headless browser fallback** — if `playwright` is installed, uses headless Chromium as a last resort\n\nWhen all methods fail, provide the `ProductDetailUrl` to the user so they can download from the Mouser product page in their browser.\n\n## Web Search Fallback\n\nIf no API key is available, search Mouser by fetching product pages directly:\n\n- Search URL: `https://www.mouser.com/c/?q=\u003cquery>`\n- Product pages contain full specs, pricing tiers, stock, datasheets\n- Results from Mouser can be noisy (JS-heavy pages)\n\nInclude key parameters in the query:\n- **Passives**: value, package (0402/0603/0805), tolerance, voltage/power rating, dielectric (C0G/X7R)\n- **ICs**: part number or function, package (QFN/SOIC/TSSOP), key specs (voltage, current, interface)\n- **Connectors**: type (USB-C, JST-PH), pin count, pitch, mounting (SMD/THT), orientation\n\n## Tips\n\n- Pipe-separate up to 10 part numbers in a single PartNumber search for batch lookups\n- `Min` and `Mult` fields matter — some parts have minimum order qty or must be ordered in multiples\n- `SurchargeMessages` may include US tariff percentages — factor into cost estimates\n- `AvailabilityOnOrder` shows incoming stock with expected dates\n- Check `IsDiscontinued` and `LifecycleStatus` before selecting parts\n---","attachment_filenames":["scripts/fetch_datasheet_mouser.py","scripts/sync_datasheets_mouser.py"],"attachments":[{"filename":"scripts/fetch_datasheet_mouser.py","content":"#!/usr/bin/env python3\n\"\"\"Download a datasheet PDF by searching Mouser for the URL.\n\nMouser's datasheet URLs (mouser.com/datasheet/...) sometimes block\nautomated downloads with bot protection. This script works around that\nby trying multiple download methods and falling back to alternative\nmanufacturer URL patterns.\n\nDownload methods (tried in order):\n - requests library (HTTP/2, redirects, anti-bot headers)\n - Python urllib (HTTP/1.1 fallback)\n - Playwright headless browser (JS-rendered pages, last resort)\n\nAfter download, the PDF is verified by extracting text and checking\nfor the MPN, manufacturer name, and description keywords.\n\nUsage:\n python3 fetch_datasheet_mouser.py --search \u003cMPN> [--output \u003cpath>]\n python3 fetch_datasheet_mouser.py \u003curl> [--output \u003cpath>]\n\nEnvironment:\n MOUSER_SEARCH_API_KEY — required for --search mode\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\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 - playwright (optional) — for JS-rendered datasheet pages\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\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\n# ---------------------------------------------------------------------------\n# Mouser API\n# ---------------------------------------------------------------------------\n\ndef _get_api_key() -> str:\n \"\"\"Get the Mouser Search API key from environment.\"\"\"\n key = os.environ.get(\"MOUSER_SEARCH_API_KEY\", \"\")\n if not key:\n print(\n \"Error: MOUSER_SEARCH_API_KEY environment variable not set.\\n\"\n \" Get a free key at mouser.com → My Account → APIs → Search API\\n\"\n \" Then: export MOUSER_SEARCH_API_KEY=your-key-here\",\n file=sys.stderr,\n )\n return key\n\n\ndef search_mouser(mpn: str, api_key: str) -> dict | None:\n \"\"\"Search Mouser API for a part by MPN. Returns first match.\"\"\"\n url = f\"https://api.mouser.com/api/v1/search/partnumber?apiKey={api_key}\"\n body = json.dumps({\n \"SearchByPartRequest\": {\n \"mouserPartNumber\": mpn,\n \"partSearchOptions\": \"Exact\",\n }\n }).encode(\"utf-8\")\n req = urllib.request.Request(\n url, data=body,\n headers={\"Content-Type\": \"application/json\"},\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=20) as resp:\n data = json.loads(resp.read())\n except Exception as e:\n print(f\"[Mouser] Search failed: {e}\", file=sys.stderr)\n return None\n\n errors = data.get(\"Errors\", [])\n if errors:\n for err in errors:\n print(f\"[Mouser] API error: {err.get('Message', err)}\", file=sys.stderr)\n return None\n\n parts = (data.get(\"SearchResults\") or {}).get(\"Parts\", [])\n if not parts:\n return None\n\n # Prefer exact MPN match\n mpn_upper = mpn.upper()\n for p in parts:\n if p.get(\"ManufacturerPartNumber\", \"\").upper() == mpn_upper:\n return p\n return parts[0]\n\n\n# ---------------------------------------------------------------------------\n# URL normalization\n# ---------------------------------------------------------------------------\n\ndef normalize_url(url: str) -> str:\n \"\"\"Convert manufacturer-specific redirect URLs to direct PDF URLs.\"\"\"\n # Protocol-relative URLs\n if url.startswith(\"//\"):\n url = \"https:\" + url\n\n # TI redirect: extract the gotoUrl parameter\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\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\n# ---------------------------------------------------------------------------\n# Download methods\n# ---------------------------------------------------------------------------\n\ndef _download_requests(url: str, output_path: str) -> bool:\n \"\"\"Download using the requests library (handles HTTP/2, all manufacturers).\"\"\"\n if _requests is None:\n return False\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 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 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 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 3. Fall back to Playwright headless browser (JS-rendered pages)\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 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 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 scrape_product_page(product_url: str) -> str:\n \"\"\"Try to extract a datasheet PDF URL from a Mouser product page.\n\n Mouser product pages contain datasheet links in the HTML. This tries\n to fetch the page and extract the link without requiring JS rendering.\n Returns the datasheet URL or empty string on failure.\n \"\"\"\n if not product_url:\n return \"\"\n\n # Normalize URL\n if not product_url.startswith(\"http\"):\n product_url = \"https://www.mouser.com\" + product_url\n\n headers = {\n \"User-Agent\": _USER_AGENT,\n \"Accept\": \"text/html,application/xhtml+xml\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n }\n\n html = \"\"\n\n # Try requests first\n if _requests is not None:\n try:\n resp = _requests.get(product_url, headers=headers, timeout=20,\n allow_redirects=True)\n if resp.status_code == 200:\n html = resp.text\n except Exception:\n pass\n\n # Fallback to urllib\n if not html:\n try:\n req = urllib.request.Request(product_url, headers=headers)\n with urllib.request.urlopen(req, timeout=20) as resp:\n html = resp.read().decode(\"utf-8\", errors=\"ignore\")\n except Exception:\n pass\n\n if not html:\n return \"\"\n\n # Look for datasheet PDF links in the HTML\n # Mouser uses patterns like href=\"/datasheet/...\" or data-href with PDF URLs\n patterns = [\n r'href=\"(https?://www\\.mouser\\.com/datasheet/[^\"]+\\.pdf[^\"]*)\"',\n r'href=\"(/datasheet/[^\"]+\\.pdf[^\"]*)\"',\n r'href=\"(https?://[^\"]+\\.pdf)\"[^>]*>[^\u003c]*[Dd]ata\\s*[Ss]heet',\n r'\"(https?://www\\.mouser\\.com/pdfDocs/[^\"]+\\.pdf)\"',\n ]\n\n for pattern in patterns:\n match = re.search(pattern, html, re.IGNORECASE)\n if match:\n url = match.group(1)\n if url.startswith(\"/\"):\n url = \"https://www.mouser.com\" + url\n return url\n\n return \"\"\n\n\ndef try_alternative_sources(mpn: str, output_path: str) -> bool:\n \"\"\"Try downloading from known alternative datasheet sources.\n\n When the primary URL fails, try manufacturer-specific patterns.\n \"\"\"\n alternatives = []\n mpn_upper = mpn.upper()\n\n # Microchip\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\n# ---------------------------------------------------------------------------\n# PDF verification\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 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 try:\n with open(pdf_path, \"rb\") as f:\n raw = f.read(200_000)\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\n - keyword_total: int\n - details: str\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\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 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 mfg_found = mfg_upper in text_upper\n if not mfg_found:\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 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\n# ---------------------------------------------------------------------------\n# Filename helpers\n# ---------------------------------------------------------------------------\n\ndef _friendly_filename(mpn: str, description: str = \"\") -> str:\n \"\"\"Build a safe filename 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\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Download a datasheet via Mouser search\",\n )\n group = parser.add_mutually_exclusive_group(required=True)\n group.add_argument(\"url\", nargs=\"?\", help=\"Direct URL to a PDF\")\n group.add_argument(\"--search\", metavar=\"MPN\", help=\"Search Mouser by MPN\")\n parser.add_argument(\"--output\", \"-o\", help=\"Output file path\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"JSON output\")\n args = parser.parse_args()\n\n if args.url:\n # Direct URL mode\n output_path = args.output or \"datasheet.pdf\"\n if download_pdf(args.url, output_path):\n if args.json:\n json.dump({\"success\": True, \"output\": output_path, \"source\": \"direct\"}, sys.stdout)\n sys.exit(0)\n else:\n print(f\"Failed to download PDF from {args.url}\", file=sys.stderr)\n if args.json:\n json.dump({\"success\": False, \"url\": args.url, \"error\": \"Download failed\"}, sys.stdout)\n sys.exit(1)\n\n # --search mode\n mpn = args.search\n result = {\"mpn\": mpn} if args.json else None\n\n # Search Mouser for the datasheet URL\n api_key = _get_api_key()\n if not api_key:\n if result:\n result.update({\"success\": False, \"error\": \"MOUSER_SEARCH_API_KEY not set\"})\n json.dump(result, sys.stdout)\n sys.exit(2)\n\n print(f\"[Mouser] Searching for {mpn}...\", file=sys.stderr)\n mouser_part = search_mouser(mpn, api_key)\n\n if not mouser_part:\n print(f\"[Mouser] No results for '{mpn}'\", file=sys.stderr)\n if result:\n result.update({\"success\": False, \"error\": f\"No Mouser results for '{mpn}'\"})\n json.dump(result, sys.stdout)\n sys.exit(2)\n\n ds_url = mouser_part.get(\"DataSheetUrl\", \"\")\n desc = mouser_part.get(\"Description\", \"\")\n mfg = mouser_part.get(\"Manufacturer\", \"\")\n\n if result:\n result[\"manufacturer\"] = mfg\n result[\"description\"] = desc\n result[\"datasheet_url\"] = ds_url\n\n output_path = args.output or (_friendly_filename(mpn, desc) + \".pdf\")\n\n def _finish_success(output_path: str, source: str):\n \"\"\"Verify the downloaded PDF and output results.\"\"\"\n vr = verify_datasheet(output_path, mpn, desc, mfg)\n if vr[\"confidence\"] == \"wrong\":\n print(f\"[Mouser] 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\"[Mouser] Verification inconclusive for {mpn}: {vr['details']}\",\n file=sys.stderr)\n\n print(f\"[Mouser] Success: {output_path}\", file=sys.stderr)\n if result:\n result.update({\n \"success\": True, \"output\": output_path,\n \"source\": source, \"verification\": vr,\n })\n json.dump(result, sys.stdout)\n sys.exit(0)\n\n # Strategy 1: Try Mouser's datasheet URL\n if ds_url:\n print(f\"[Mouser] Trying Mouser datasheet URL...\", file=sys.stderr)\n if download_pdf(ds_url, output_path):\n _finish_success(output_path, \"mouser\")\n else:\n print(f\"[Mouser] Mouser URL blocked or failed\", file=sys.stderr)\n\n # Strategy 2: Scrape Mouser product page for datasheet link\n product_url = mouser_part.get(\"ProductDetailUrl\", \"\")\n if product_url:\n print(f\"[Mouser] Scraping product page for datasheet link...\", file=sys.stderr)\n scraped_url = scrape_product_page(product_url)\n if scraped_url:\n print(f\"[Mouser] Found datasheet link on product page\", file=sys.stderr)\n if download_pdf(scraped_url, output_path):\n _finish_success(output_path, \"mouser_scrape\")\n\n # Strategy 3: Try alternative manufacturer sources\n print(f\"[Mouser] Trying alternative sources...\", file=sys.stderr)\n if try_alternative_sources(mpn, output_path):\n _finish_success(output_path, \"alternative\")\n\n # All methods failed — provide manual URL\n print(f\"[Mouser] Failed to download datasheet for {mpn}\", file=sys.stderr)\n if product_url:\n print(f\"[Mouser] Manual download: {product_url}\", file=sys.stderr)\n if result:\n result.update({\"success\": False, \"error\": \"All download methods failed\"})\n if product_url:\n result[\"manual_url\"] = product_url\n json.dump(result, 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":20886,"content_sha256":"4585d6bc18a2eeb882b9bc83480ae0178b2b3db9528054f03dd02abe3d287a3d"},{"filename":"scripts/sync_datasheets_mouser.py","content":"#!/usr/bin/env python3\n\"\"\"Sync a local datasheets directory for a KiCad project via Mouser.\n\nExtracts components with MPNs from a KiCad schematic (or pre-computed\nanalyzer JSON), searches Mouser for datasheet URLs, downloads missing\nPDFs, and maintains a manifest.json file (legacy name index.json still read).\n\nThe manifest.json format matches across distributor skills so they can\ncontribute to the same datasheets directory. The source field\ndistinguishes which distributor provided the datasheet.\n\nDownload strategy per part:\n 1. Try the datasheet URL from the schematic itself\n 2. Search Mouser API for the part → try Mouser's datasheet URL\n 3. Try manufacturer-specific alternative URL patterns\n\nDownload methods (per URL, tried in order):\n - requests library (HTTP/2, redirects, anti-bot headers)\n - Python urllib (HTTP/1.1 fallback)\n - Playwright headless browser (JS-rendered pages, last resort)\n\nUsage:\n python3 sync_datasheets_mouser.py \u003cfile.kicad_sch>\n python3 sync_datasheets_mouser.py \u003canalyzer_output.json> --output ./datasheets\n python3 sync_datasheets_mouser.py \u003cfile.kicad_sch> --force # retry failures\n python3 sync_datasheets_mouser.py \u003cfile.kicad_sch> --dry-run # preview only\n python3 sync_datasheets_mouser.py --mpn-list mpns.txt --dry-run\n python3 sync_datasheets_mouser.py --mpn-list mpns.txt --output ./datasheets\n\nRequires MOUSER_SEARCH_API_KEY environment variable.\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\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n# Import from sibling script (same skill)\nsys.path.insert(0, str(Path(__file__).parent))\nfrom fetch_datasheet_mouser import (\n download_pdf,\n normalize_url,\n scrape_product_page,\n try_alternative_sources,\n verify_datasheet,\n search_mouser,\n _get_api_key,\n)\n\n\n# ---------------------------------------------------------------------------\n# MPN filtering — distinguish real manufacturer part numbers from generic values\n# ---------------------------------------------------------------------------\n\n_GENERIC_VALUE_RE = re.compile(\n r\"^[\\d.]+\\s*[pnuμmkMGR]?[FHΩRfhω]?$\"\n r\"|^[\\d.]+\\s*[kKmM]?[Ωω]?$\"\n r\"|^[\\d.]+\\s*[pnuμm]?[Ff]$\"\n r\"|^[\\d.]+\\s*[pnuμm]?[Hh]$\"\n r\"|^[\\d.]+%$\"\n r\"|^DNP$|^NC$|^N/?A$\",\n re.IGNORECASE,\n)\n\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 has_letter = any(c.isalpha() for c in mpn)\n has_digit = any(c.isdigit() for c in mpn)\n return has_letter and has_digit\n\n\n# ---------------------------------------------------------------------------\n# Filename sanitization — matches convention across distributor skills\n# ---------------------------------------------------------------------------\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Convert a string to a safe filename component (without extension).\"\"\"\n name = re.sub(r'[/\\\\:*?\"\u003c>|,;]', \"_\", name)\n name = re.sub(r\"\\s+\", \"_\", name)\n name = re.sub(r\"_+\", \"_\", name).strip(\"_\")\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 \"\"\"\n base = sanitize_filename(mpn)\n if not description:\n return base\n desc = description.strip()\n if manufacturer and desc.lower().endswith(manufacturer.lower()):\n desc = desc[: -len(manufacturer)].strip().rstrip(\",\").strip()\n if len(desc) > 80:\n desc = desc[:77].rsplit(\"_\", 1)[0].rsplit(\" \", 1)[0]\n desc = sanitize_filename(desc)\n return f\"{base}_{desc}\" if desc else base\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 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 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 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 Mouser PN,\n a DigiKey PN, an LCSC PN, or an element14 PN. Users set up their KiCad\n projects differently — some only have MPNs, some only have distributor PNs,\n 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 mouser_pn = entry.get(\"mouser\", \"\").strip()\n digikey_pn = entry.get(\"digikey\", \"\").strip()\n lcsc_pn = entry.get(\"lcsc\", \"\").strip()\n element14_pn = entry.get(\"element14\", \"\").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(mouser_pn or digikey_pn or lcsc_pn or element14_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 \"mouser\": mouser_pn,\n \"digikey\": digikey_pn,\n \"lcsc\": lcsc_pn,\n \"element14\": element14_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 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 \"mouser\": \"\",\n \"digikey\": \"\",\n \"lcsc\": \"\",\n \"element14\": \"\",\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 mouser_api_key: str,\n index: dict,\n delay: float,\n) -> dict:\n \"\"\"Download datasheet for one part. Returns updated manifest entry.\"\"\"\n mpn = part[\"mpn\"]\n mouser_pn = part.get(\"mouser\", \"\")\n now = datetime.now(timezone.utc).isoformat()\n\n # Use MPN for display/filename if available, otherwise fall back to Mouser PN\n display_pn = mpn or mouser_pn\n\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 Mouser API — try MPN first, then Mouser PN\n time.sleep(delay) # Rate limit\n search_query = mpn or mouser_pn\n print(f\" Searching Mouser for '{search_query}'...\", file=sys.stderr)\n mouser_part = search_mouser(search_query, mouser_api_key)\n\n # If MPN search failed but we have a Mouser PN, try that\n if not mouser_part and mpn and mouser_pn:\n time.sleep(delay)\n print(f\" MPN not found, trying Mouser PN '{mouser_pn}'...\", file=sys.stderr)\n mouser_part = search_mouser(mouser_pn, mouser_api_key)\n\n mouser_ds_url = \"\"\n if mouser_part:\n mouser_ds_url = mouser_part.get(\"DataSheetUrl\", \"\")\n mouser_mfg = mouser_part.get(\"Manufacturer\", mfg)\n mouser_desc = mouser_part.get(\"Description\", \"\")\n if mouser_desc:\n filename = friendly_filename(display_pn, mouser_desc, mouser_mfg) + \".pdf\"\n output_path = output_dir / filename\n desc = mouser_desc\n mfg = mouser_mfg\n\n # Try Mouser's datasheet URL\n if mouser_ds_url:\n print(f\" Trying Mouser datasheet URL...\", file=sys.stderr)\n if download_pdf(mouser_ds_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\": mouser_ds_url,\n \"downloaded_date\": now,\n \"source\": \"mouser\",\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 3: Scrape Mouser product page for datasheet link\n if mouser_part:\n product_url = mouser_part.get(\"ProductDetailUrl\", \"\")\n if product_url:\n print(f\" Scraping product page for datasheet link...\", file=sys.stderr)\n scraped_url = scrape_product_page(product_url)\n if scraped_url:\n print(f\" Found datasheet link on product page\", file=sys.stderr)\n if download_pdf(scraped_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\": scraped_url,\n \"downloaded_date\": now,\n \"source\": \"mouser_scrape\",\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 manufacturer sources\n if display_pn:\n print(f\" Trying alternative sources...\", file=sys.stderr)\n if display_pn and try_alternative_sources(display_pn, str(output_path)):\n size = os.path.getsize(str(output_path))\n vr = verify_datasheet(str(output_path), display_pn, desc, mfg)\n result = {\n \"file\": filename,\n \"manufacturer\": mfg,\n \"description\": desc,\n \"value\": part[\"value\"],\n \"datasheet_url\": mouser_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 if not mouser_part:\n return {\n \"manufacturer\": mfg,\n \"description\": desc,\n \"value\": part[\"value\"],\n \"references\": part[\"references\"],\n \"status\": \"not_found\",\n \"error\": f\"No Mouser results for '{display_pn}'\",\n \"last_attempt\": now,\n }\n\n return {\n \"manufacturer\": mfg,\n \"description\": desc,\n \"value\": part[\"value\"],\n \"datasheet_url\": mouser_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 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 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 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_mpn = 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 parts = extract_parts(analyzer_output)\n all_bom = analyzer_output.get(\"bom\", [])\n skipped_no_mpn = 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 )\n\n print(f\"Found {len(parts)} unique parts with MPNs \"\n f\"({skipped_no_mpn} skipped without MPN)\", file=sys.stderr)\n\n to_download = []\n already_present = []\n skipped_failed = []\n\n for part in parts:\n mpn = part[\"mpn\"]\n mouser_pn = part.get(\"mouser\", \"\")\n display_pn = mpn or mouser_pn\n existing = index.get(\"parts\", {}).get(display_pn, {})\n status = existing.get(\"status\", \"\")\n\n if status == \"ok\":\n old_file = existing.get(\"file\", \"\")\n if (out_dir / old_file).exists():\n if not force_all:\n if not dry_run:\n desc = existing.get(\"description\", \"\") or part.get(\"description\", \"\")\n mfg_name = existing.get(\"manufacturer\", \"\") or part.get(\"manufacturer\", \"\")\n new_file = friendly_filename(display_pn, desc, mfg_name) + \".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(display_pn)\n existing[\"references\"] = part[\"references\"]\n continue\n\n if status in (\"failed\", \"not_found\", \"no_datasheet\") and not (force or force_all):\n skipped_failed.append(display_pn)\n continue\n\n to_download.append(part)\n\n if dry_run:\n summary = {\n \"would_download\": [p[\"mpn\"] or p.get(\"mouser\", \"\") for p in to_download],\n \"already_present\": already_present,\n \"skipped_previous_failures\": skipped_failed,\n \"skipped_no_mpn\": skipped_no_mpn,\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 pn = p[\"mpn\"] or p.get(\"mouser\", \"\")\n print(f\" {pn} ({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 MPN): {skipped_no_mpn}\")\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 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 mouser_api_key = _get_api_key()\n if not mouser_api_key:\n return {\"error\": \"MOUSER_SEARCH_API_KEY not set\"}\n\n downloaded = []\n failed = []\n warnings = []\n\n if parallel > 1:\n lock = threading.Lock()\n counter = [0]\n\n def _process_part(part):\n display_pn = part[\"mpn\"] or part.get(\"mouser\", \"\")\n with lock:\n counter[0] += 1\n n = counter[0]\n print(f\"[{n}/{len(to_download)}] {display_pn}\", file=sys.stderr)\n\n result = sync_one_part(part, out_dir, mouser_api_key, index, delay)\n\n with lock:\n index.setdefault(\"parts\", {})[display_pn] = result\n\n if result[\"status\"] == \"ok\":\n downloaded.append(display_pn)\n vconf = result.get(\"verification\", \"\")\n vmark = \"\"\n if vconf == \"wrong\":\n vmark = \" ⚠ WRONG DATASHEET?\"\n warnings.append(display_pn)\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(display_pn)\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 display_pn = part[\"mpn\"] or part.get(\"mouser\", \"\")\n print(f\"[{i+1}/{len(to_download)}] {display_pn}\", file=sys.stderr)\n\n result = sync_one_part(part, out_dir, mouser_api_key, index, delay)\n\n index.setdefault(\"parts\", {})[display_pn] = result\n\n if result[\"status\"] == \"ok\":\n downloaded.append(display_pn)\n vconf = result.get(\"verification\", \"\")\n vmark = \"\"\n if vconf == \"wrong\":\n vmark = \" ⚠ WRONG DATASHEET?\"\n warnings.append(display_pn)\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(display_pn)\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 \"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_mpn\": skipped_no_mpn,\n \"total_parts_with_mpn\": 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 MPN): {skipped_no_mpn}\", file=sys.stderr)\n print(f\" Output: {out_dir}/\", file=sys.stderr)\n\n return summary\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Sync datasheets for a KiCad project via Mouser 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 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 sys.exit(0)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":30139,"content_sha256":"3891926b73d65fc05d91fd2ecee7036cf57fb243a08fe5b1d4d7763268a00b29"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Mouser Electronics 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 part lookup","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":"digikey","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Primary prototype source (prefer for datasheets — direct PDF links)","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 Mouser parametric data for behavioral SPICE models","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Mouser is the ","type":"text"},{"text":"secondary source for prototype orders","type":"text","marks":[{"type":"strong"}]},{"text":" — use when DigiKey is out of stock or has worse pricing. 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":". For datasheets, prefer DigiKey's API (direct PDF links) — Mouser blocks automated PDF downloads.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"API Credential Setup","type":"text"}]},{"type":"paragraph","content":[{"text":"Mouser uses ","type":"text"},{"text":"simple API key authentication","type":"text","marks":[{"type":"strong"}]},{"text":" — no OAuth, no tokens, no callback URLs. The Search API key is a UUID passed as a query parameter.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Getting Your API Key","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go to ","type":"text"},{"text":"mouser.com","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.mouser.com","title":null}}]},{"text":" → My Mouser → My Account","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Under \"APIs\" section, click \"Manage\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Register for ","type":"text"},{"text":"Search API","type":"text","marks":[{"type":"strong"}]},{"text":" key — this is the one needed for part lookups and datasheet downloads","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search API keys may require approval (status shows \"pending authorization\" initially)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Setting Credentials","type":"text"}]},{"type":"paragraph","content":[{"text":"Set the environment variable before running the scripts:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"export MOUSER_SEARCH_API_KEY=your-search-api-key-uuid","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 '^

Mouser Electronics Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for part lookup | | | BOM management — orchestrates sourcing across distributors | | | Primary prototype source (prefer for datasheets — direct PDF links) | | | Uses Mouser parametric data for behavioral SPICE models | Mouser is the secondary source for prototype orders — use when DigiKey is out of stock or has worse pricing. For production orders, see / . For BOM management and export workflows, see . For datasheets, prefer DigiKey's API (direct PDF links)…

| xargs)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Mouser Search API Reference","type":"text"}]},{"type":"paragraph","content":[{"text":"All search endpoints use the Search API key as a query parameter: ","type":"text"},{"text":"?apiKey=\u003ckey>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Content-Type is ","type":"text"},{"text":"application/json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for all POST requests.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"V1 Endpoints","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Keyword Search","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /api/v1/search/keyword?apiKey=\u003ckey>","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"SearchByKeywordRequest\": {\n \"keyword\": \"100nF 0402 ceramic capacitor\",\n \"records\": 50,\n \"startingRecord\": 0,\n \"searchOptions\": \"InStock\"\n }\n}","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"searchOptions","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"\"None\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"\"Rohs\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"\"InStock\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"\"RohsAndInStock\"","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"records","type":"text","marks":[{"type":"code_inline"}]},{"text":": max 50 per request","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"startingRecord","type":"text","marks":[{"type":"code_inline"}]},{"text":": offset for pagination","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Part Number Search","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /api/v1/search/partnumber?apiKey=\u003ckey>","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"SearchByPartRequest\": {\n \"mouserPartNumber\": \"GRM155R71C104KA88D|RC0402FR-0710KL\",\n \"partSearchOptions\": \"Exact\"\n }\n}","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Up to ","type":"text"},{"text":"10 part numbers","type":"text","marks":[{"type":"strong"}]},{"text":", pipe-separated (","type":"text"},{"text":"|","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Works with both Mouser part numbers AND manufacturer part numbers (MPNs)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"partSearchOptions","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"\"Exact\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"\"BeginsWith\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"\"Contains\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"V2 Endpoints","type":"text"}]},{"type":"paragraph","content":[{"text":"V2 adds manufacturer filtering and pagination by page number.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Keyword + Manufacturer Search","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /api/v2/search/keywordandmanufacturer?apiKey=\u003ckey>","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"SearchByKeywordMfrNameRequest\": {\n \"keyword\": \"LMR51450\",\n \"manufacturerName\": \"Texas Instruments\",\n \"records\": 25,\n \"pageNumber\": 1,\n \"searchOptions\": \"InStock\"\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Note: the wrapper object name is ","type":"text"},{"text":"SearchByKeywordMfrNameRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not ","type":"text"},{"text":"SearchByKeywordMfrRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the V1 name is deprecated).","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Part Number + Manufacturer Search","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /api/v2/search/partnumberandmanufacturer?apiKey=\u003ckey>","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Manufacturer List","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"GET /api/v2/search/manufacturerlist?apiKey=\u003ckey>","type":"text"}]},{"type":"paragraph","content":[{"text":"Returns the full list of manufacturer names for use in filtered searches.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"V1 Deprecated Endpoints","type":"text"}]},{"type":"paragraph","content":[{"text":"These still work but V2 equivalents are preferred:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"POST /api/v1/search/keywordandmanufacturer","type":"text","marks":[{"type":"code_inline"}]},{"text":" → use V2","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"POST /api/v1/search/partnumberandmanufacturer","type":"text","marks":[{"type":"code_inline"}]},{"text":" → use V2","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GET /api/v1/search/manufacturerlist","type":"text","marks":[{"type":"code_inline"}]},{"text":" → use V2","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Search Response Structure","type":"text"}]},{"type":"paragraph","content":[{"text":"All search endpoints return the same response format:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"Errors\": [],\n \"SearchResults\": {\n \"NumberOfResult\": 142,\n \"Parts\": [...]\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Key Part Fields","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":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","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":"MouserPartNumber","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mouser's internal part number (prefixed, e.g., ","type":"text"},{"text":"81-GRM155R71C104KA88","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":"ManufacturerPartNumber","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manufacturer's part number (MPN) — use for cross-distributor matching","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manufacturer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manufacturer name","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Product description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Product category","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DataSheetUrl","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URL to datasheet PDF (Mouser-hosted — see Datasheet section)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ProductDetailUrl","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URL to Mouser product page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ImagePath","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Product image URL","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Availability","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Human-readable stock text (e.g., \"2648712 In Stock\")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AvailabilityInStock","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Numeric stock quantity (as string)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AvailabilityOnOrder","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Incoming stock: ","type":"text"},{"text":"[{Quantity, Date}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LeadTime","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Factory lead time (e.g., \"84 Days\")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LifecycleStatus","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string|null","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"New Product\", \"End of Life\", etc.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IsDiscontinued","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"true\" or \"false\" (string, not boolean)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SuggestedReplacement","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replacement MPN if discontinued","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Min","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Minimum order quantity (as string)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mult","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Order multiple (as string)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reeling","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bool","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tape-and-reel packaging available","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ROHSStatus","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RoHS compliance status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PriceBreaks","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tiered pricing: ","type":"text"},{"text":"[{Quantity, Price, Currency}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ProductAttributes","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Parametric specs: ","type":"text"},{"text":"[{AttributeName, AttributeValue}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AlternatePackagings","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array|null","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Alternate packaging MPNs: ","type":"text"},{"text":"[{APMfrPN}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SurchargeMessages","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tariff/surcharge info: ","type":"text"},{"text":"[{code, message}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ProductCompliance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTS codes, ECCN: ","type":"text"},{"text":"[{ComplianceName, ComplianceValue}]","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"UnitWeightKg","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"object","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{UnitWeight: \u003cfloat>}","type":"text","marks":[{"type":"code_inline"}]},{"text":" in kg","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Quirks and Gotchas","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Price is a string","type":"text","marks":[{"type":"strong"}]},{"text":" with currency symbol: ","type":"text"},{"text":"\"$0.10\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", not a float. Parse it before comparing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stock is a string","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"AvailabilityInStock","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns ","type":"text"},{"text":"\"2648712\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" not ","type":"text"},{"text":"2648712","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IsDiscontinued is a string","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"\"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"false\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", not boolean.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mouser part numbers have prefixes","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"81-GRM155R71C104KA88","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the prefix is Mouser-specific. Use ","type":"text"},{"text":"ManufacturerPartNumber","type":"text","marks":[{"type":"code_inline"}]},{"text":" for cross-referencing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"V2 wrapper names differ from V1","type":"text","marks":[{"type":"strong"}]},{"text":": V2 uses ","type":"text"},{"text":"SearchByKeywordMfrNameRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":", not ","type":"text"},{"text":"SearchByKeywordMfrRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tariff info","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"SurchargeMessages","type":"text","marks":[{"type":"code_inline"}]},{"text":" may contain US tariff percentages — useful for cost estimation.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On-order data","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"AvailabilityOnOrder","type":"text","marks":[{"type":"code_inline"}]},{"text":" shows incoming stock quantities and expected dates.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Datasheet Download & Sync","type":"text"}]},{"type":"paragraph","content":[{"text":"Mouser's ","type":"text"},{"text":"DataSheetUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" field points to Mouser-hosted URLs (","type":"text"},{"text":"mouser.com/datasheet/...","type":"text","marks":[{"type":"code_inline"}]},{"text":"). These URLs ","type":"text"},{"text":"block automated downloads","type":"text","marks":[{"type":"strong"}]},{"text":" — they return an HTML \"Access denied\" page when fetched with Python/curl/wget, even with browser User-Agent headers. The download scripts handle this with a multi-strategy approach:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Try the Mouser datasheet URL directly (works for some parts)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scrape the Mouser product page HTML for alternative datasheet links","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Try manufacturer-specific alternative URL patterns","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Note: Mouser's product pages return 403 for most automated requests, so strategy 2 has limited success. DigiKey and LCSC are more reliable datasheet sources.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Datasheet Directory Sync","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"sync_datasheets_mouser.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. Same workflow and ","type":"text"},{"text":"manifest.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" format as the DigiKey skill.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Sync datasheets for a KiCad project\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch>\n\n# Preview what would be downloaded\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> --dry-run\n\n# Retry previously failed downloads\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> --force\n\n# Custom output directory\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.py \u003cfile.kicad_sch> -o ./my-datasheets\n\n# Parallel downloads (3 workers)\npython3 \u003cskill-path>/scripts/sync_datasheets_mouser.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_mouser.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 part-library seeding). One MPN per line; blank lines and ","type":"text"},{"text":"#","type":"text","marks":[{"type":"code_inline"}]},{"text":" comments (full-line and inline) are skipped; generic values (","type":"text"},{"text":"100nF","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","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":"heading","attrs":{"level":3},"content":[{"text":"Single Datasheet Download","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"fetch_datasheet_mouser.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" for one-off downloads.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Search by MPN (uses Mouser API)\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py --search \"TPS61023DRLR\" -o datasheet.pdf\n\n# Direct URL download\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py \"https://example.com/datasheet.pdf\" -o datasheet.pdf\n\n# JSON output\npython3 \u003cskill-path>/scripts/fetch_datasheet_mouser.py --search \"ADP1706\" --json","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Download Strategy","type":"text"}]},{"type":"paragraph","content":[{"text":"The scripts try multiple sources in order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Schematic URL","type":"text","marks":[{"type":"strong"}]},{"text":" — uses the datasheet URL embedded in the KiCad symbol (sync only)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mouser API search","type":"text","marks":[{"type":"strong"}]},{"text":" — gets the ","type":"text"},{"text":"DataSheetUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the API response","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Alternative manufacturer sources","type":"text","marks":[{"type":"strong"}]},{"text":" — tries known URL patterns for major manufacturers (TI, Microchip, etc.) when the Mouser URL is blocked","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, uses headless Chromium as a last resort","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When all methods fail, provide the ","type":"text"},{"text":"ProductDetailUrl","type":"text","marks":[{"type":"code_inline"}]},{"text":" to the user so they can download from the Mouser product page in their browser.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Web Search Fallback","type":"text"}]},{"type":"paragraph","content":[{"text":"If no API key is available, search Mouser by fetching product pages directly:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search URL: ","type":"text"},{"text":"https://www.mouser.com/c/?q=\u003cquery>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Product pages contain full specs, pricing tiers, stock, datasheets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Results from Mouser can be noisy (JS-heavy pages)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Include key parameters in the query:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Passives","type":"text","marks":[{"type":"strong"}]},{"text":": value, package (0402/0603/0805), tolerance, voltage/power rating, dielectric (C0G/X7R)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ICs","type":"text","marks":[{"type":"strong"}]},{"text":": part number or function, package (QFN/SOIC/TSSOP), key specs (voltage, current, interface)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Connectors","type":"text","marks":[{"type":"strong"}]},{"text":": type (USB-C, JST-PH), pin count, pitch, mounting (SMD/THT), orientation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tips","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipe-separate up to 10 part numbers in a single PartNumber search for batch lookups","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Min","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"Mult","type":"text","marks":[{"type":"code_inline"}]},{"text":" fields matter — some parts have minimum order qty or must be ordered in multiples","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SurchargeMessages","type":"text","marks":[{"type":"code_inline"}]},{"text":" may include US tariff percentages — factor into cost estimates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AvailabilityOnOrder","type":"text","marks":[{"type":"code_inline"}]},{"text":" shows incoming stock with expected dates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check ","type":"text"},{"text":"IsDiscontinued","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"LifecycleStatus","type":"text","marks":[{"type":"code_inline"}]},{"text":" before selecting parts","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"mouser","author":"@skillopedia","source":{"stars":464,"repo_name":"kicad-happy","origin_url":"https://github.com/aklofas/kicad-happy/blob/HEAD/skills/mouser/SKILL.md","repo_owner":"aklofas","body_sha256":"36047aad39d21b3919c5a6d3ea61e094060608ba3d7ee30799ab1a3711e7b503","cluster_key":"4311e1d4c9ddf35cb17262a60873d4776375620c320b9c3118d59c94896c098a","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aklofas/kicad-happy/skills/mouser/SKILL.md","attachments":[{"id":"ce7c2352-e3c6-5527-8a85-757689c0f190","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce7c2352-e3c6-5527-8a85-757689c0f190/attachment.py","path":"scripts/fetch_datasheet_mouser.py","size":20886,"sha256":"4585d6bc18a2eeb882b9bc83480ae0178b2b3db9528054f03dd02abe3d287a3d","contentType":"text/x-python; charset=utf-8"},{"id":"1bd547ce-67e9-57db-ae55-588c9aeffc28","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1bd547ce-67e9-57db-ae55-588c9aeffc28/attachment.py","path":"scripts/sync_datasheets_mouser.py","size":30139,"sha256":"3891926b73d65fc05d91fd2ecee7036cf57fb243a08fe5b1d4d7763268a00b29","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"6053c142b9a68f9d9b891b93c8b22365b158ff7e694a2457df23f18e2bec3f8f","attachment_count":2,"text_attachments":2,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/mouser/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 Mouser Electronics for electronic components — secondary source for prototype orders. Find parts, check pricing/stock, download datasheets, analyze specifications. Use with KiCad for BOM creation and part selection. Also supports batch MPN-list seeding (`--mpn-list`) for bulk datasheet workflows without a KiCad project. Use this skill when the user specifically mentions Mouser, when DigiKey is out of stock or has worse pricing, when comparing prices across distributors, or when searching for parts that DigiKey doesn't carry. For package cross-reference tables and BOM workflow, see the `bom` skill."}},"renderedAt":1782980375731}

Mouser Electronics Parts Search & Analysis Related Skills | Skill | Purpose | |-------|---------| | | Schematic analysis — extracts MPNs for part lookup | | | BOM management — orchestrates sourcing across distributors | | | Primary prototype source (prefer for datasheets — direct PDF links) | | | Uses Mouser parametric data for behavioral SPICE models | Mouser is the secondary source for prototype orders — use when DigiKey is out of stock or has worse pricing. For production orders, see / . For BOM management and export workflows, see . For datasheets, prefer DigiKey's API (direct PDF links)…