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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- 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]
|