ecadlib 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/providers.py ADDED
@@ -0,0 +1,226 @@
1
+ """Pluggable part-source providers for BOM triage.
2
+
3
+ Provider interface: lookup(line) -> SourceResult (raise NotConfigured if credentials missing)
4
+ Implemented (unauthenticated, as used by easyeda2kicad 1.0.1):
5
+ * JLCPCBSearch - POST keyword search, resolves MPN -> LCSC number
6
+ * EasyEDA - GET component JSON by LCSC number; reports symbol / footprint / 3D presence
7
+ Stubs (document auth, raise NotConfigured without keys): DigiKey, Mouser, Nexar, SnapMagic, SamacSys.
8
+ All response bodies are untrusted: parsed as JSON only, fields read defensively."""
9
+ import json, os
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ class NotConfigured(Exception):
14
+ pass
15
+
16
+
17
+ def clean_text(v, limit=250):
18
+ """One line of plain text from an untrusted field (None / non-text -> ''), cut at `limit` characters."""
19
+ if not isinstance(v, str):
20
+ return ""
21
+ v = " ".join(v.split())
22
+ return v if len(v) <= limit else v[:limit - 3].rstrip() + "..."
23
+
24
+
25
+ def describe(text="", category="", package=""):
26
+ """The part description a source gives: its own description text, else '<category>, <package>' when it names a
27
+ category (both are the distributor's words; nothing is made up). '' when it gives neither."""
28
+ text = clean_text(text)
29
+ if text:
30
+ return text
31
+ category = clean_text(category, 120)
32
+ return ", ".join(x for x in (category, clean_text(package, 80)) if x) if category else ""
33
+
34
+
35
+ @dataclass
36
+ class SourceResult:
37
+ provider: str
38
+ found: bool | None = None # None = could not determine (error)
39
+ lcsc: str = ""
40
+ mpn: str = ""
41
+ manufacturer: str = ""
42
+ package: str = ""
43
+ has_symbol: bool | None = None
44
+ has_footprint: bool | None = None
45
+ has_3d: bool | None = None
46
+ url: str = ""
47
+ error: str = ""
48
+ extra: dict = field(default_factory=dict)
49
+
50
+
51
+ class Provider:
52
+ name = "base"
53
+ auth = "none" # human-readable description of the credentials needed
54
+ env = () # environment variables that must be set
55
+
56
+ def __init__(self, http):
57
+ self.http = http
58
+
59
+ def configured(self):
60
+ return all(os.environ.get(e) for e in self.env)
61
+
62
+ def lookup(self, line) -> SourceResult:
63
+ raise NotImplementedError
64
+
65
+
66
+ # ------------------------------------------------------------------ EasyEDA (LCSC id -> CAD data)
67
+ class EasyEDA(Provider):
68
+ name = "easyeda"
69
+ auth = "none (public endpoint used by easyeda2kicad; Referer https://easyeda.com/ header)"
70
+ # easyeda2kicad 1.0.1: API_ENDPOINT = "https://easyeda.com/api/products/{lcsc_id}/components"
71
+ # (older releases appended ?version=6.4.19.5; kept here since the server ignores/accepts it)
72
+ URL = "https://easyeda.com/api/products/{}/components?version=6.4.19.5"
73
+ HEADERS = {"Accept": "application/json, text/javascript, */*; q=0.01", "Referer": "https://easyeda.com/"}
74
+
75
+ def lookup(self, line):
76
+ lcsc = (line.get("lcsc") or "").strip().upper()
77
+ if not lcsc:
78
+ return SourceResult(self.name, found=None, error="no LCSC number")
79
+ url = self.URL.format(lcsc)
80
+ r = self.http.get(url, headers=self.HEADERS)
81
+ res = SourceResult(self.name, lcsc=lcsc, url=f"https://www.lcsc.com/product-detail/{lcsc}.html")
82
+ if not r.ok:
83
+ res.error = r.error or f"HTTP {r.status}"
84
+ return res
85
+ try:
86
+ return parse_easyeda(r.json(), res)
87
+ except (ValueError, AttributeError, TypeError) as e:
88
+ res.error = f"unparseable response: {e}"
89
+ return res
90
+
91
+
92
+ def parse_easyeda(data, res):
93
+ """Field paths follow easyeda2kicad's importer: result.dataStr.head.c_para (symbol),
94
+ result.packageDetail.dataStr (footprint), 'SVGNODE~{json}' shape with c_etype outline3D (3D)."""
95
+ if not isinstance(data, dict) or data.get("success") is False or not data.get("result"):
96
+ res.found = False
97
+ return res
98
+ r = data["result"]
99
+ ds = r.get("dataStr") or {}
100
+ cpara = (ds.get("head") or {}).get("c_para") or {}
101
+ pkg = r.get("packageDetail") or {}
102
+ pshape = (pkg.get("dataStr") or {}).get("shape") or []
103
+ res.found = True
104
+ res.mpn = cpara.get("Manufacturer Part") or cpara.get("BOM_Manufacturer Part") or r.get("title", "")
105
+ res.manufacturer = cpara.get("Manufacturer") or cpara.get("BOM_Manufacturer") or ""
106
+ res.package = cpara.get("package") or pkg.get("title", "")
107
+ res.has_symbol = bool(ds.get("shape")) or bool(r.get("subparts"))
108
+ res.has_footprint = bool(pshape)
109
+ res.has_3d = False
110
+ for s in pshape:
111
+ if isinstance(s, str) and s.startswith("SVGNODE~"):
112
+ try:
113
+ node = json.loads(s.split("~", 1)[1])
114
+ except ValueError:
115
+ continue
116
+ attrs = node.get("attrs", {})
117
+ if attrs.get("c_etype") == "outline3D" or attrs.get("uuid"):
118
+ res.has_3d = True
119
+ res.extra["model_uuid"] = attrs.get("uuid", "")
120
+ res.extra["footprint_name"] = pkg.get("title", "")
121
+ tags = r.get("tags") if isinstance(r.get("tags"), list) else []
122
+ desc = describe(r.get("description"), next((t for t in tags if isinstance(t, str) and t.strip()), ""), res.package)
123
+ if desc: # result.description (LCSC's text), else its category tag + package
124
+ res.extra["description"] = desc
125
+ return res
126
+
127
+
128
+ # ------------------------------------------------------------------ JLCPCB parts search (MPN -> LCSC)
129
+ class JLCPCBSearch(Provider):
130
+ name = "jlcpcb_search"
131
+ auth = "none (anonymous JSON POST; Origin/Referer jlcpcb.com) - as used by easyeda2kicad search"
132
+ URL = "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList"
133
+ HEADERS = {"Content-Type": "application/json", "Origin": "https://jlcpcb.com", "Referer": "https://jlcpcb.com/parts"}
134
+ REFUSED = {403: "JLCPCB refused the request (403)", 429: "JLCPCB refused the request (429, too many requests)"}
135
+
136
+ def post(self, keyword, size=10):
137
+ """One search request. JLCPCB answers 403 (or 429) when it blocks automated searches: that is reported as
138
+ 'JLCPCB refused the request (403)' and the rest of the run sends JLCPCB nothing more (no retries, no hammering;
139
+ the refusal is kept on the HTTP client, so every JLCPCB source sharing it stops). Callers go on to their other
140
+ sources."""
141
+ from .httpcache import Response
142
+ refused = getattr(self.http, "_jlcpcb_refused", None)
143
+ if refused:
144
+ return Response(refused[0], "", error=f"{refused[1]}; not asked again this run")
145
+ r = self.http.post(self.URL, json.dumps({"keyword": keyword, "currentPage": 1, "pageSize": size}), headers=self.HEADERS)
146
+ if r.status in self.REFUSED and not r.ok:
147
+ msg = self.REFUSED[r.status]
148
+ try:
149
+ self.http._jlcpcb_refused = (r.status, msg)
150
+ except AttributeError: # a client that takes no attributes: nothing remembered
151
+ pass
152
+ return Response(r.status, r.text, error=f"{msg}: the site blocks automated part searches from here; "
153
+ "other sources are still tried (set the LCSC number in the BOM to skip the search)")
154
+ return r
155
+
156
+ def lookup(self, line):
157
+ mpn = (line.get("mpn") or "").strip()
158
+ if not mpn:
159
+ return SourceResult(self.name, found=None, error="no MPN")
160
+ r = self.post(mpn)
161
+ res = SourceResult(self.name, mpn=mpn)
162
+ if not r.ok:
163
+ res.error = r.error or f"HTTP {r.status}"
164
+ return res
165
+ try:
166
+ from .kicad_index import norm_mpn
167
+ items = (((r.json().get("data") or {}).get("componentPageInfo") or {}).get("list")) or []
168
+ exact = [i for i in items if norm_mpn(i.get("componentModelEn")) == norm_mpn(mpn)]
169
+ except (ValueError, AttributeError, TypeError) as e: # untrusted body of an unexpected shape
170
+ res.error = f"unparseable: {e}"
171
+ return res
172
+ best = (exact or [None])[0]
173
+ res.found = bool(best)
174
+ if best:
175
+ res.lcsc = best.get("componentCode", "")
176
+ res.manufacturer = best.get("componentBrandEn", "")
177
+ res.package = best.get("componentSpecificationEn", "")
178
+ res.extra = {"stock": best.get("stockCount"), "library_type": best.get("componentLibraryType")}
179
+ desc = describe(best.get("describe") or best.get("description"),
180
+ best.get("secondSortName") or best.get("firstSortName"), res.package)
181
+ if desc: # JLCPCB/LCSC's description of the part, else its category + package
182
+ res.extra["description"] = desc
183
+ return res
184
+
185
+
186
+ # ------------------------------------------------------------------ authenticated distributor / CAD stubs
187
+ class _Stub(Provider):
188
+ docs = ""
189
+
190
+ def lookup(self, line):
191
+ if not self.configured():
192
+ raise NotConfigured(f"{self.name}: set {', '.join(self.env)} ({self.auth})")
193
+ raise NotImplementedError(f"{self.name}: credentials present but client not implemented in prototype. {self.docs}")
194
+
195
+
196
+ class DigiKey(_Stub):
197
+ name, env = "digikey", ("DIGIKEY_CLIENT_ID", "DIGIKEY_CLIENT_SECRET")
198
+ auth = "OAuth2 client-credentials app from developer.digikey.com (Product Information API v4)"
199
+ docs = "POST https://api.digikey.com/products/v4/search/keyword with X-DIGIKEY-Client-Id + Bearer token."
200
+
201
+
202
+ class Mouser(_Stub):
203
+ name, env = "mouser", ("MOUSER_API_KEY",)
204
+ auth = "Search API key (free, mouser.com/api-hub) passed as ?apiKey="
205
+ docs = "POST https://api.mouser.com/api/v1/search/partnumber?apiKey=KEY {SearchByPartRequest:{mouserPartNumber}}."
206
+
207
+
208
+ class Nexar(_Stub):
209
+ name, env = "nexar", ("NEXAR_CLIENT_ID", "NEXAR_CLIENT_SECRET")
210
+ auth = "OAuth2 client-credentials (identity.nexar.com) for the Octopart/Nexar GraphQL API; quota-limited"
211
+ docs = "POST https://api.nexar.com/graphql query supSearchMpn(q:...) {results{part{mpn manufacturer{name} cadModels...}}}."
212
+
213
+
214
+ class SnapMagic(_Stub):
215
+ name, env = "snapmagic", ("SNAPMAGIC_API_TOKEN",)
216
+ auth = "SnapMagic (ex-SnapEDA) partner API token - no self-serve public API; CAD downloads need a user login"
217
+ docs = "Partner/enterprise API by agreement; otherwise manual download from snapeda.com."
218
+
219
+
220
+ class SamacSys(_Stub):
221
+ name, env = "samacsys", ("SAMACSYS_USERNAME", "SAMACSYS_PASSWORD")
222
+ auth = "componentsearchengine.com account; access is via Library Loader / partner API, no documented public REST"
223
+ docs = "Library Loader desktop app or supplier-embedded (Mouser/RS 'ECAD model') links."
224
+
225
+
226
+ ALL = [JLCPCBSearch, EasyEDA, DigiKey, Mouser, Nexar, SnapMagic, SamacSys]