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/httpcache.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tiny disk-cached, per-host rate-limited HTTP client on top of requests.
|
|
2
|
+
|
|
3
|
+
Responses are stored as JSON files keyed by sha256(method, url, body). Only 2xx/404
|
|
4
|
+
responses are cached (not a 2xx whose JSON body is an `errors` list); network errors are returned (not raised) so callers can
|
|
5
|
+
report "source unreachable" instead of crashing. Downloaded bodies are treated as
|
|
6
|
+
untrusted data: they are only ever parsed as JSON/text, never executed."""
|
|
7
|
+
import hashlib, json, os, re, threading, time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
UA = "ecad-librarian-prototype/0.1 (+https://github.com/; contact: maintainers)"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Response:
|
|
17
|
+
def __init__(self, status, text, from_cache=False, error=None):
|
|
18
|
+
self.status, self.text, self.from_cache, self.error = status, text, from_cache, error
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def ok(self):
|
|
22
|
+
return self.error is None and 200 <= self.status < 300
|
|
23
|
+
|
|
24
|
+
def json(self):
|
|
25
|
+
return json.loads(self.text)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SECRET_Q = re.compile(r"(api[_-]?key|key|token|secret|password|signature|sig|client[_-]?secret|access[_-]?token)$", re.I)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
VARY_H = re.compile(r"locale|currency|language", re.I)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_ERRORS = re.compile(r'"[eE]rrors"\s*:\s*[\[{"]')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def error_body(text):
|
|
38
|
+
"""True for a 2xx JSON answer that is really an error: GraphQL `errors` (Nexar: bad token, quota, a field the plan
|
|
39
|
+
does not include) or Mouser's `Errors` list (bad API key, daily limit). It is not cached: the key or the quota
|
|
40
|
+
may be fixed by the next run, and a cached error would come back for the whole TTL."""
|
|
41
|
+
if not _ERRORS.search(text[:65536]):
|
|
42
|
+
return False
|
|
43
|
+
try:
|
|
44
|
+
d = json.loads(text)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return False
|
|
47
|
+
return isinstance(d, dict) and bool(d.get("errors") or d.get("Errors"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def redact(url):
|
|
51
|
+
u = urlparse(url)
|
|
52
|
+
q = [(k, "REDACTED" if SECRET_Q.search(k) else v) for k, v in parse_qsl(u.query, keep_blank_values=True)]
|
|
53
|
+
return urlunparse(u._replace(query=urlencode(q))) if q else url
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CachedHTTP:
|
|
57
|
+
def __init__(self, cache_dir="out/http_cache", min_interval=1.0, ttl_s=7 * 86400,
|
|
58
|
+
offline=False, timeout=20):
|
|
59
|
+
self.dir = Path(cache_dir)
|
|
60
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
from . import migrate as MG # a cache stamped by a newer ecadlib is refused (never misread)
|
|
62
|
+
MG.open_folder(self.dir, ["http_cache"], write=False)
|
|
63
|
+
self.min_interval, self.ttl, self.offline, self.timeout = min_interval, ttl_s, offline, timeout
|
|
64
|
+
self._last = {} # host -> last request time
|
|
65
|
+
self.s = requests.Session()
|
|
66
|
+
self.s.headers["User-Agent"] = UA
|
|
67
|
+
self.log = [] # (method, url, status/error, cached)
|
|
68
|
+
|
|
69
|
+
def _key(self, method, url, body, headers=None):
|
|
70
|
+
# headers that change the answer (DigiKey's X-DIGIKEY-Locale-Currency: the same URL priced in EUR or USD) are
|
|
71
|
+
# part of the key; auth headers never are (tokens rotate, and must not decide what is cached)
|
|
72
|
+
vary = sorted((k.lower(), str(v)) for k, v in (headers or {}).items() if VARY_H.search(k))
|
|
73
|
+
extra = "".join(f"\n{k}: {v}" for k, v in vary)
|
|
74
|
+
return hashlib.sha256(f"{method}\n{url}\n{body or ''}{extra}".encode()).hexdigest()[:32]
|
|
75
|
+
|
|
76
|
+
def request(self, method, url, body=None, headers=None, cache=True, timeout=None):
|
|
77
|
+
"""cache=False: never read or store (credentials in the URL or body, tokens in the response). timeout: seconds
|
|
78
|
+
for this request (default: the client's).
|
|
79
|
+
Query-string secrets (apiKey=, token=...) are redacted from cache keys and stored URLs.
|
|
80
|
+
A 2xx answer whose body is an error (error_body) is neither stored nor served from an older cache entry."""
|
|
81
|
+
f = self.dir / f"{self._key(method, redact(url), body, headers)}.json"
|
|
82
|
+
d = None
|
|
83
|
+
bad = lambda st, text: 200 <= st < 300 and error_body(text)
|
|
84
|
+
if cache and f.exists() and (self.offline or time.time() - f.stat().st_mtime < self.ttl): # offline: stale beats nothing
|
|
85
|
+
try:
|
|
86
|
+
d = json.loads(f.read_text(encoding="utf-8"))
|
|
87
|
+
d = d if isinstance(d, dict) and isinstance(d.get("status"), int) and isinstance(d.get("text"), str) else None
|
|
88
|
+
except (OSError, ValueError): # a damaged entry (crash, full disk) is a miss, not a crash
|
|
89
|
+
d = None
|
|
90
|
+
if d is not None and bad(d["status"], d["text"]):
|
|
91
|
+
d = None
|
|
92
|
+
if d is not None:
|
|
93
|
+
self.log.append((method, url, d["status"], True))
|
|
94
|
+
return Response(d["status"], d["text"], from_cache=True)
|
|
95
|
+
if self.offline:
|
|
96
|
+
self.log.append((method, url, "offline-miss", False))
|
|
97
|
+
return Response(0, "", error="offline: not in cache")
|
|
98
|
+
host = urlparse(url).netloc
|
|
99
|
+
wait = self.min_interval - (time.time() - self._last.get(host, 0))
|
|
100
|
+
if wait > 0:
|
|
101
|
+
time.sleep(wait)
|
|
102
|
+
self._last[host] = time.time()
|
|
103
|
+
try:
|
|
104
|
+
from .term import waiting, source_name # a slow source says so on the terminal
|
|
105
|
+
with waiting(source_name(host)):
|
|
106
|
+
r = self.s.request(method, url, data=body, headers=headers, timeout=timeout or self.timeout)
|
|
107
|
+
except requests.RequestException as e:
|
|
108
|
+
m = re.search(r"Tunnel connection failed: ([^'\")]+)", str(e))
|
|
109
|
+
msg = f"{type(e).__name__}: " + (f"proxy CONNECT refused ({m.group(1).strip()})" if m else str(e)[:120])
|
|
110
|
+
self.log.append((method, url, msg, False))
|
|
111
|
+
return Response(0, "", error=msg)
|
|
112
|
+
self.log.append((method, url, r.status_code, False))
|
|
113
|
+
if cache and (200 <= r.status_code < 300 or r.status_code == 404) and not bad(r.status_code, r.text):
|
|
114
|
+
tmp = f.with_name(f"{f.stem}.{os.getpid()}.{threading.get_ident()}.tmp") # own temp file per writer: two
|
|
115
|
+
tmp.write_text(json.dumps({"url": redact(url), "status": r.status_code, "text": r.text}), encoding="utf-8")
|
|
116
|
+
os.replace(tmp, f) # threads fetching one URL never share one
|
|
117
|
+
return Response(r.status_code, r.text, error=None if r.ok or r.status_code == 404
|
|
118
|
+
else f"HTTP {r.status_code}")
|
|
119
|
+
|
|
120
|
+
def get(self, url, **kw):
|
|
121
|
+
return self.request("GET", url, **kw)
|
|
122
|
+
|
|
123
|
+
def post(self, url, body, **kw):
|
|
124
|
+
return self.request("POST", url, body=body, **kw)
|