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/eval/golden.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Golden output snapshots: normalised fingerprints of the KiCad and Altium files ecadlib writes (`ecadlib eval golden`).
|
|
2
|
+
|
|
3
|
+
ecadlib eval golden rebuild the parts and compare with ecadlib/eval/golden/*.txt (exit 1 + diff on a change)
|
|
4
|
+
ecadlib eval golden --update rewrite the snapshots (commit them: a deliberate change is one reviewed diff)
|
|
5
|
+
ecadlib eval golden --group specs one group only (testpack, demo, specs)
|
|
6
|
+
|
|
7
|
+
Groups (all offline, no API key):
|
|
8
|
+
testpack the six parts of `ecadlib testpack` (librarian `standard` + the pack's KiCad library, STEP bodies on)
|
|
9
|
+
demo the bundled demo BOM through Workspace.librarian_run + kicad_dblib (`ecadlib demo`)
|
|
10
|
+
specs footprint_gen / footprint_adv on every package_specs/*.json, advanced/*.json and every 20th entry of the
|
|
11
|
+
standard table, written the librarian's way (kicad_import.convert_footprint -> altium_lib.write_pcblib)
|
|
12
|
+
|
|
13
|
+
A fingerprint is text, one line per thing that matters, so a diff reads like a review: footprint properties, every pad
|
|
14
|
+
(number, type, shape, position, size, drill, layers, rotation), graphics per layer as count + bounding box + hash,
|
|
15
|
+
3D model; symbol fields and every pin (number, name, type, position, length); Altium pads in mm and every Altium
|
|
16
|
+
parameter; DbLib / database-library rows. Left out, because they are not geometry or fields: uuids / tstamps,
|
|
17
|
+
generator stamps, Altium UNIQUEIDs, GUIDs, MODELID / MODEL.CHECKSUM, file-header DATE / TIME (Altium files are read
|
|
18
|
+
back with altium_lib's reader, so the OLE container's timestamps never enter). Numbers are rounded to 4 decimals."""
|
|
19
|
+
import argparse, csv, difflib, hashlib, io, json, os, re, sqlite3, sys, tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
HERE = Path(__file__).parent / "golden"
|
|
23
|
+
GROUPS = ("testpack", "demo", "specs")
|
|
24
|
+
SPEC_STRIDE = 20 # every 20th standard-table entry (sorted names): 31 of 615
|
|
25
|
+
DROP_HEADS = {"uuid", "tstamp", "generator", "generator_version"}
|
|
26
|
+
GRAPHICS = {"fp_line", "fp_rect", "fp_circle", "fp_arc", "fp_poly", "fp_curve", "rectangle", "polyline", "circle", "arc",
|
|
27
|
+
"bezier", "gr_line", "gr_rect", "gr_circle", "gr_arc", "gr_poly"}
|
|
28
|
+
VOLATILE = re.compile(r"(UNIQUEID|GUID|MODELID|CHECKSUM|^DATE$|^TIME$)", re.I)
|
|
29
|
+
NUM = re.compile(r"^-?\d+(\.\d+)?(e-?\d+)?$")
|
|
30
|
+
BARE = re.compile(r"^[A-Za-z0-9_.*&+:/-]+$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------- normalising
|
|
34
|
+
def n(v):
|
|
35
|
+
"""A number as short stable text (4 decimals, no trailing zeros, no -0)."""
|
|
36
|
+
x = round(float(v), 4)
|
|
37
|
+
if x == 0:
|
|
38
|
+
x = 0.0
|
|
39
|
+
s = f"{x:.4f}".rstrip("0").rstrip(".")
|
|
40
|
+
return s
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _atom(a):
|
|
44
|
+
a = str(a)
|
|
45
|
+
if NUM.match(a):
|
|
46
|
+
return n(a)
|
|
47
|
+
return a if BARE.match(a) else json.dumps(a)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def canon(node):
|
|
51
|
+
"""S-expression -> one line, uuids / tstamps dropped, numbers normalised."""
|
|
52
|
+
if not isinstance(node, list):
|
|
53
|
+
return _atom(node)
|
|
54
|
+
return "(" + " ".join(canon(c) for c in node if not (isinstance(c, list) and c and c[0] in DROP_HEADS)) + ")"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _points(node, out):
|
|
58
|
+
if isinstance(node, list):
|
|
59
|
+
if node and node[0] in ("start", "end", "mid", "center", "xy", "at") and len(node) >= 3:
|
|
60
|
+
try:
|
|
61
|
+
out.append((float(node[1]), float(node[2])))
|
|
62
|
+
except ValueError:
|
|
63
|
+
pass
|
|
64
|
+
for c in node[1:]:
|
|
65
|
+
_points(c, out)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _layer(node):
|
|
70
|
+
from .. import sexpr
|
|
71
|
+
l = sexpr.first(node, "layer")
|
|
72
|
+
return str(l[1]) if l and len(l) > 1 else "-"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _summary(kind, items, layer_of):
|
|
76
|
+
"""Graphics of one kind per layer: count, bounding box, hash of their canonical text."""
|
|
77
|
+
groups = {}
|
|
78
|
+
for it in items:
|
|
79
|
+
groups.setdefault(layer_of(it), []).append(canon(it) if isinstance(it, list) else it)
|
|
80
|
+
out = []
|
|
81
|
+
for lay, cs in sorted(groups.items()):
|
|
82
|
+
pts = [p for it in items if layer_of(it) == lay for p in (_points(it, []) if isinstance(it, list) else [])]
|
|
83
|
+
box = f" bbox({n(min(p[0] for p in pts))},{n(min(p[1] for p in pts))},{n(max(p[0] for p in pts))},{n(max(p[1] for p in pts))})" if pts else ""
|
|
84
|
+
h = hashlib.sha256("\n".join(cs).encode()).hexdigest()[:10]
|
|
85
|
+
out.append(f"graphics {kind} {lay}: {len(cs)}{box} #{h}")
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
MANY_PADS = 16 # more pads than this (BGAs, big QFPs): pin 1 in full, the rest grouped by everything but position
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _pads(pads):
|
|
93
|
+
"""pads: [(number, x, y, rest, full line)] -> lines. Few pads: every line. Many: pad 1 / A1 in full, then one line
|
|
94
|
+
per kind (same rest: shape, size, drill, layers ...) with count, bounding box of the centres and a hash of the lines."""
|
|
95
|
+
if len(pads) <= MANY_PADS:
|
|
96
|
+
return [p[4] for p in pads]
|
|
97
|
+
out = [p[4] for p in pads if p[0] in ("1", "A1")][:1]
|
|
98
|
+
kinds = {}
|
|
99
|
+
for p in pads:
|
|
100
|
+
kinds.setdefault(p[3], []).append(p)
|
|
101
|
+
for rest, ps in sorted(kinds.items()):
|
|
102
|
+
h = hashlib.sha256("\n".join(p[4] for p in ps).encode()).hexdigest()[:10]
|
|
103
|
+
out.append(f"pads x{len(ps)} {rest} centres bbox({n(min(p[1] for p in ps))},{n(min(p[2] for p in ps))},"
|
|
104
|
+
f"{n(max(p[1] for p in ps))},{n(max(p[2] for p in ps))}) #{h}")
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def kicad_mod(text):
|
|
109
|
+
from .. import sexpr
|
|
110
|
+
t = sexpr.parse(text)
|
|
111
|
+
lines, gfx, pads = [f"footprint {_atom(t[1])}"], {}, []
|
|
112
|
+
for c in t[2:]:
|
|
113
|
+
if not isinstance(c, list) or not c or c[0] in DROP_HEADS:
|
|
114
|
+
continue
|
|
115
|
+
if c[0] == "pad":
|
|
116
|
+
at = sexpr.first(c, "at") or ["at", "0", "0"]
|
|
117
|
+
rest = canon([x for x in c if not (isinstance(x, list) and x and x[0] == "at")][2:])
|
|
118
|
+
pads.append((str(c[1]), float(at[1]), float(at[2]), rest, canon(c)))
|
|
119
|
+
elif c[0] in GRAPHICS:
|
|
120
|
+
gfx.setdefault(c[0], []).append(c)
|
|
121
|
+
elif c[0] in ("property", "fp_text"):
|
|
122
|
+
at = sexpr.first(c, "at")
|
|
123
|
+
lines.append(f"{c[0]} " + " ".join(_atom(x) for x in c[1:] if not isinstance(x, list)) +
|
|
124
|
+
f" layer={_layer(c)}" + (f" at={canon(at)}" if at else "") +
|
|
125
|
+
(" hidden" if any(x == "hide" or (isinstance(x, list) and x[:2] == ["hide", "yes"]) for x in c) else ""))
|
|
126
|
+
else:
|
|
127
|
+
lines.append(canon(c))
|
|
128
|
+
lines += _pads(pads)
|
|
129
|
+
for k in sorted(gfx):
|
|
130
|
+
lines += _summary(k, gfx[k], _layer)
|
|
131
|
+
return lines
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def kicad_sym(text):
|
|
135
|
+
"""-> {symbol name: lines}."""
|
|
136
|
+
from .. import sexpr
|
|
137
|
+
t = sexpr.parse(text)
|
|
138
|
+
out = {}
|
|
139
|
+
for s in sexpr.find(t, "symbol"):
|
|
140
|
+
lines, gfx, pins = [], {}, []
|
|
141
|
+
|
|
142
|
+
def walk(node, unit):
|
|
143
|
+
for c in node[2:] if node is not s else node[2:]:
|
|
144
|
+
if not isinstance(c, list) or not c:
|
|
145
|
+
continue
|
|
146
|
+
if c[0] == "symbol":
|
|
147
|
+
walk(c, str(c[1]))
|
|
148
|
+
elif c[0] == "pin":
|
|
149
|
+
nm, no, at, ln = sexpr.first(c, "name") or ["", ""], sexpr.first(c, "number") or ["", ""], \
|
|
150
|
+
sexpr.first(c, "at"), sexpr.first(c, "length") or ["", "?"]
|
|
151
|
+
hid = any(x == "hide" or (isinstance(x, list) and x[:2] == ["hide", "yes"]) for x in c)
|
|
152
|
+
pins.append(f"pin {_atom(no[1])} {_atom(nm[1])} {c[1]} {c[2]} unit={unit} at={canon(at)} length={_atom(ln[1])}"
|
|
153
|
+
+ (" hidden" if hid else ""))
|
|
154
|
+
elif c[0] in GRAPHICS or c[0] == "text":
|
|
155
|
+
gfx.setdefault(c[0], []).append([*c, ["layer", f"unit{unit}"]])
|
|
156
|
+
elif c[0] == "property":
|
|
157
|
+
at = sexpr.first(c, "at")
|
|
158
|
+
hid = any(x == "hide" or (isinstance(x, list) and x[:2] == ["hide", "yes"]) for x in sexpr.first(c, "effects") or []) \
|
|
159
|
+
or any(isinstance(x, list) and x[:2] == ["hide", "yes"] for x in c)
|
|
160
|
+
lines.append(f"property {_atom(c[1])}={_atom(c[2])}" + (f" at={canon(at)}" if at else "") + (" hidden" if hid else ""))
|
|
161
|
+
elif node is s:
|
|
162
|
+
lines.append(canon(c))
|
|
163
|
+
walk(s, "0")
|
|
164
|
+
for k in sorted(gfx):
|
|
165
|
+
lines += _summary(k, gfx[k], _layer)
|
|
166
|
+
out[str(s[1])] = lines + pins
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _mm(u):
|
|
171
|
+
from .. import altium_lib as AL
|
|
172
|
+
return n(AL.to_mm(u))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _params(d):
|
|
176
|
+
return " ".join(f"{k}={v}" for k, v in sorted(d.items()) if not VOLATILE.search(k))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def pcblib(path):
|
|
180
|
+
"""-> {footprint name: lines} from altium_lib's reader (records, not bytes)."""
|
|
181
|
+
from .. import altium_lib as AL
|
|
182
|
+
out = {}
|
|
183
|
+
for fp in AL.read_pcblib(path):
|
|
184
|
+
lines, gfx, pads = [f"params {_params(fp['params'])}"], {}, []
|
|
185
|
+
for r in fp["records"]:
|
|
186
|
+
t = r["type"]
|
|
187
|
+
if t == "pad":
|
|
188
|
+
num = r["designator"] or '""'
|
|
189
|
+
rest = (f"layer={r['layer']} size=({_mm(r['w'])},{_mm(r['h'])}) hole={_mm(r['hole'])} shape={r['shape']} "
|
|
190
|
+
f"rot={n(r['rot'])} corner={r['corner']} plated={r['plated']} paste={r['paste_mode']}/{_mm(r['paste'] or 0)} "
|
|
191
|
+
f"mask={r['mask_mode']}/{_mm(r['mask'] or 0)} hole_shape={r['hole_shape']} slot={_mm(r['slot'])} "
|
|
192
|
+
f"hole_rot={n(r['hole_rot'])}")
|
|
193
|
+
pads.append((r["designator"], AL.to_mm(r["x"]), AL.to_mm(r["y"]), rest, f"pad {num} at=({_mm(r['x'])},{_mm(r['y'])}) {rest}"))
|
|
194
|
+
elif t == "text":
|
|
195
|
+
lines.append(f"text {json.dumps(r['text'])} layer={r['layer']} at=({_mm(r['x'])},{_mm(r['y'])}) h={_mm(r['h'])} "
|
|
196
|
+
f"w={_mm(r['w'])} rot={n(r['rot'])} mirror={r['mirror']}")
|
|
197
|
+
elif t == "body":
|
|
198
|
+
p = "|".join(x for x in r["params"].split("|") if not VOLATILE.search(x.split("=")[0]))
|
|
199
|
+
lines.append(f"body layer={r['layer']} {p} pts=" + ";".join(f"{_mm(x)},{_mm(y)}" for x, y in r["pts"]))
|
|
200
|
+
else:
|
|
201
|
+
if t in ("track", "fill"):
|
|
202
|
+
pts, txt = [(r["x1"], r["y1"]), (r["x2"], r["y2"])], f"{t} {r['x1']} {r['y1']} {r['x2']} {r['y2']} {r.get('w')}"
|
|
203
|
+
elif t == "arc":
|
|
204
|
+
pts = [(r["cx"] - r["r"], r["cy"] - r["r"]), (r["cx"] + r["r"], r["cy"] + r["r"])]
|
|
205
|
+
txt = f"arc {r['cx']} {r['cy']} {r['r']} {n(r['a0'])} {n(r['a1'])} {r['w']}"
|
|
206
|
+
elif t == "region":
|
|
207
|
+
pts, txt = list(r["pts"]), f"region {r['params']} " + ";".join(f"{n(x)},{n(y)}" for x, y in r["pts"])
|
|
208
|
+
else:
|
|
209
|
+
pts, txt = [], f"{t} {r['raw'][0].hex()}"
|
|
210
|
+
gfx.setdefault((t, r["layer"]), []).append((pts, txt))
|
|
211
|
+
lines += _pads(pads)
|
|
212
|
+
for (t, lay), items in sorted(gfx.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))):
|
|
213
|
+
pts = [p for ps, _ in items for p in ps]
|
|
214
|
+
box = f" bbox({_mm(min(p[0] for p in pts))},{_mm(min(p[1] for p in pts))},{_mm(max(p[0] for p in pts))},{_mm(max(p[1] for p in pts))})" if pts else ""
|
|
215
|
+
lines.append(f"graphics {t} {lay}: {len(items)}{box} #{hashlib.sha256(chr(10).join(x for _, x in items).encode()).hexdigest()[:10]}")
|
|
216
|
+
for prm, data in fp.get("models") or []:
|
|
217
|
+
lines.append(f"model {_params(prm)} bytes={len(data)} sha256={hashlib.sha256(data).hexdigest()[:16]}")
|
|
218
|
+
out[fp["name"]] = lines
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def schlib(path):
|
|
223
|
+
"""-> {symbol name: lines}: component fields, every pin, graphics as count + hash."""
|
|
224
|
+
from .. import altium_lib as AL
|
|
225
|
+
_, syms = AL.read_schlib(path)
|
|
226
|
+
out = {}
|
|
227
|
+
for s in syms:
|
|
228
|
+
lines, gfx = [], {}
|
|
229
|
+
for r in s["records"]:
|
|
230
|
+
rec = r.get("RECORD")
|
|
231
|
+
if rec == "2" and "num" in r:
|
|
232
|
+
lines.append(f"pin {r['num']} {json.dumps(r['name'])} etype={r['etype']} orient={r['orient']} at=({r['x']},{r['y']}) "
|
|
233
|
+
f"length={r['length']} part={r['part']} mode={r['mode']} symbols={list(r['symbols'])} cong={r['conglomerate']}")
|
|
234
|
+
elif rec in ("1", "34", "41", "45", "44", "46", "48"):
|
|
235
|
+
lines.append(f"record{rec} " + _params({k: v for k, v in r.items() if k not in ("RECORD", "INDEXINSHEET")}))
|
|
236
|
+
else:
|
|
237
|
+
gfx.setdefault(rec, []).append(_params({k: v for k, v in r.items() if k not in ("INDEXINSHEET",)}))
|
|
238
|
+
for rec, items in sorted(gfx.items(), key=lambda kv: str(kv[0])):
|
|
239
|
+
lines.append(f"graphics record{rec}: {len(items)} #{hashlib.sha256(chr(10).join(items).encode()).hexdigest()[:10]}")
|
|
240
|
+
out[s["name"]] = lines
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def table_rows(path):
|
|
245
|
+
"""A CSV (DbLib table, parts export) or SQLite file -> sorted rows as lines."""
|
|
246
|
+
path = Path(path)
|
|
247
|
+
if path.suffix == ".sqlite":
|
|
248
|
+
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
249
|
+
try:
|
|
250
|
+
out = []
|
|
251
|
+
for (t,) in con.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"):
|
|
252
|
+
cur = con.execute(f'SELECT * FROM "{t}"')
|
|
253
|
+
cols = [d[0] for d in cur.description]
|
|
254
|
+
out.append(f"table {t}: " + ",".join(cols))
|
|
255
|
+
out += sorted(f"row {t}: " + json.dumps(list(r), ensure_ascii=False) for r in cur)
|
|
256
|
+
return out
|
|
257
|
+
finally:
|
|
258
|
+
con.close()
|
|
259
|
+
rows = list(csv.reader(io.StringIO(path.read_text(encoding="utf-8-sig"))))
|
|
260
|
+
return ["header " + json.dumps(rows[0], ensure_ascii=False)] + sorted("row " + json.dumps(r, ensure_ascii=False) for r in rows[1:]) if rows else []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def fingerprint_file(path, rel):
|
|
264
|
+
"""One output file -> {key: lines} (keys carry the relative path)."""
|
|
265
|
+
p = Path(path)
|
|
266
|
+
if p.suffix == ".kicad_mod":
|
|
267
|
+
return {rel: kicad_mod(p.read_text(encoding="utf-8"))}
|
|
268
|
+
if p.suffix == ".kicad_sym":
|
|
269
|
+
return {f"{rel}:{k}": v for k, v in kicad_sym(p.read_text(encoding="utf-8")).items()}
|
|
270
|
+
if p.suffix == ".PcbLib":
|
|
271
|
+
return {f"{rel}:{k}": v for k, v in pcblib(p).items()}
|
|
272
|
+
if p.suffix == ".SchLib":
|
|
273
|
+
return {f"{rel}:{k}": v for k, v in schlib(p).items()}
|
|
274
|
+
if p.suffix in (".csv", ".sqlite"):
|
|
275
|
+
return {rel: table_rows(p)}
|
|
276
|
+
raise ValueError(f"no fingerprint for {p.suffix}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def fingerprint_tree(root, patterns):
|
|
280
|
+
root, out = Path(root), {}
|
|
281
|
+
for pat in patterns:
|
|
282
|
+
for f in sorted(root.glob(pat)):
|
|
283
|
+
out.update(fingerprint_file(f, f.relative_to(root).as_posix()))
|
|
284
|
+
return dict(sorted(out.items()))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------- the groups
|
|
288
|
+
def _quiet():
|
|
289
|
+
return lambda *a, **k: None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def build_testpack(tmp):
|
|
293
|
+
from .. import testpack
|
|
294
|
+
out = Path(tmp) / "testpack"
|
|
295
|
+
testpack.build(out, log=_quiet())
|
|
296
|
+
return fingerprint_tree(out, ["library/models/KiCad/*.kicad_mod", "library/models/PcbLib/*.PcbLib",
|
|
297
|
+
"library/models/SchLib/*.SchLib", "kicad/*.kicad_sym", "kicad/*.sqlite", "altium/*.csv"])
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def build_demo(tmp):
|
|
301
|
+
from .. import firstrun
|
|
302
|
+
from ..app import Workspace
|
|
303
|
+
root = Path(tmp) / "demo"
|
|
304
|
+
firstrun.build_demo(Workspace(root, offline=True), "both", log=_quiet())
|
|
305
|
+
return fingerprint_tree(root, ["librarian/*/models/KiCad/*.kicad_mod", "librarian/*/models/PcbLib/*.PcbLib",
|
|
306
|
+
"librarian/*/models/SchLib/*.SchLib", "kicad/*/*.kicad_sym", "kicad/*/*.sqlite"])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def spec_names():
|
|
310
|
+
from .. import footprint_bench as FB, stdparts
|
|
311
|
+
own = sorted(p.stem for p in FB.SPECS.glob("*.json")) + sorted(p.stem for p in (FB.SPECS / "advanced").glob("*.json"))
|
|
312
|
+
std = sorted(stdparts.packages())[::SPEC_STRIDE]
|
|
313
|
+
return [("spec", x) for x in own] + [("standard", x) for x in std if x not in own]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_specs(tmp):
|
|
317
|
+
from .. import altium_lib as AL, footprint_bench as FB, footprint_gen as F, kicad_import as K, rules as RU, stdparts
|
|
318
|
+
out, d = {}, Path(tmp) / "specs"
|
|
319
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
layers = RU.layers(RU.load({}))
|
|
321
|
+
for kind, name in spec_names():
|
|
322
|
+
spec = json.loads(FB.spec_path(name).read_text(encoding="utf-8")) if kind == "spec" else stdparts.spec(name)
|
|
323
|
+
try:
|
|
324
|
+
text, _ = F.build(spec)
|
|
325
|
+
except Exception as e: # a spec the generator rejects is part of the snapshot too
|
|
326
|
+
out[f"{kind}/{name}"] = [f"build failed: {type(e).__name__}: {e}"]
|
|
327
|
+
continue
|
|
328
|
+
out[f"{kind}/{name}.kicad_mod"] = kicad_mod(text)
|
|
329
|
+
fp, t = K.convert_footprint(text, name, kind, [])
|
|
330
|
+
try:
|
|
331
|
+
K.add_body(fp, t, None, None, False, [], gen_step=False, spec=spec)
|
|
332
|
+
except ValueError: # no height known: no body (as the librarian writes it)
|
|
333
|
+
pass
|
|
334
|
+
pcb = d / f"{AL.file_stem(name)}.PcbLib"
|
|
335
|
+
AL.write_pcblib(pcb, [fp], layers)
|
|
336
|
+
out.update({f"{kind}/{name}.PcbLib:{k}": v for k, v in pcblib(pcb).items()})
|
|
337
|
+
return dict(sorted(out.items()))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
BUILDERS = {"testpack": build_testpack, "demo": build_demo, "specs": build_specs}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def build(group):
|
|
344
|
+
env = {k: v for k, v in os.environ.items() if not k.endswith("_API_KEY")}
|
|
345
|
+
from unittest import mock
|
|
346
|
+
with tempfile.TemporaryDirectory(prefix="ecadlib-golden-") as tmp, mock.patch.dict(os.environ, env, clear=True):
|
|
347
|
+
cwd = os.getcwd()
|
|
348
|
+
os.chdir(tmp) # no ecadlib.toml of the caller's folder can change the outputs
|
|
349
|
+
try:
|
|
350
|
+
return BUILDERS[group](tmp)
|
|
351
|
+
finally:
|
|
352
|
+
os.chdir(cwd)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------- files, diff
|
|
356
|
+
def dumps(fps):
|
|
357
|
+
out = []
|
|
358
|
+
for k, lines in fps.items():
|
|
359
|
+
out.append(f"### {k}")
|
|
360
|
+
out += lines
|
|
361
|
+
return "\n".join(out) + "\n"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def loads(text):
|
|
365
|
+
fps, cur = {}, None
|
|
366
|
+
for line in text.splitlines():
|
|
367
|
+
if line.startswith("### "):
|
|
368
|
+
cur = fps.setdefault(line[4:], [])
|
|
369
|
+
elif cur is not None:
|
|
370
|
+
cur.append(line)
|
|
371
|
+
return fps
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def path(group, d=None):
|
|
375
|
+
return Path(d or HERE) / f"{group}.txt"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def diff(old, new, limit=80):
|
|
379
|
+
"""-> readable text: items added / removed, and a unified diff of each changed item (first `limit` lines)."""
|
|
380
|
+
out = []
|
|
381
|
+
for k in sorted(set(old) - set(new)):
|
|
382
|
+
out.append(f"- removed: {k}")
|
|
383
|
+
for k in sorted(set(new) - set(old)):
|
|
384
|
+
out.append(f"+ added: {k}")
|
|
385
|
+
for k in sorted(set(old) & set(new)):
|
|
386
|
+
if old[k] != new[k]:
|
|
387
|
+
out.append(f"~ changed: {k}")
|
|
388
|
+
out += [" " + l for l in list(difflib.unified_diff(old[k], new[k], "golden", "now", n=0, lineterm=""))[2:]]
|
|
389
|
+
if len(out) > limit:
|
|
390
|
+
out = out[:limit] + [f"... {len(out) - limit} more lines"]
|
|
391
|
+
return "\n".join(out)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def check(groups=GROUPS, d=None):
|
|
395
|
+
"""-> {group: diff text ('' when the same)}."""
|
|
396
|
+
res = {}
|
|
397
|
+
for g in groups:
|
|
398
|
+
f = path(g, d)
|
|
399
|
+
old = loads(f.read_text(encoding="utf-8")) if f.exists() else {}
|
|
400
|
+
new = build(g)
|
|
401
|
+
res[g] = diff(old, new) if old != new else ""
|
|
402
|
+
if not f.exists():
|
|
403
|
+
res[g] = f"no snapshot {f}: run `ecadlib eval golden --update`\n" + res[g]
|
|
404
|
+
return res
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def update(groups=GROUPS, d=None):
|
|
408
|
+
out = {}
|
|
409
|
+
for g in groups:
|
|
410
|
+
f = path(g, d)
|
|
411
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
fps = build(g)
|
|
413
|
+
f.write_text(dumps(fps), encoding="utf-8")
|
|
414
|
+
out[g] = (f, len(fps))
|
|
415
|
+
return out
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def main(argv=None):
|
|
419
|
+
ap = argparse.ArgumentParser(prog="ecadlib eval golden", description=__doc__.split("\n")[0])
|
|
420
|
+
ap.add_argument("--update", action="store_true", help="rewrite the snapshots from this build")
|
|
421
|
+
ap.add_argument("--group", help=f"comma list of {', '.join(GROUPS)} (default all)")
|
|
422
|
+
ap.add_argument("--dir", help="snapshot folder (default ecadlib/eval/golden)")
|
|
423
|
+
a = ap.parse_args(argv)
|
|
424
|
+
groups = [g.strip() for g in a.group.split(",")] if a.group else list(GROUPS)
|
|
425
|
+
bad = [g for g in groups if g not in GROUPS]
|
|
426
|
+
if bad:
|
|
427
|
+
ap.error(f"unknown group {bad[0]!r}; groups: {', '.join(GROUPS)}")
|
|
428
|
+
if a.update:
|
|
429
|
+
for g, (f, k) in update(groups, a.dir).items():
|
|
430
|
+
print(f"{g}: {k} items -> {f}")
|
|
431
|
+
print("review the diff (git diff ecadlib/eval/golden) and commit it with the change that caused it")
|
|
432
|
+
return 0
|
|
433
|
+
res, rc = check(groups, a.dir), 0
|
|
434
|
+
for g, txt in res.items():
|
|
435
|
+
if txt:
|
|
436
|
+
rc = 1
|
|
437
|
+
print(f"golden {g}: CHANGED\n{txt}")
|
|
438
|
+
else:
|
|
439
|
+
print(f"golden {g}: same")
|
|
440
|
+
if rc:
|
|
441
|
+
print("\nIf the change is deliberate: `ecadlib eval golden --update`, review `git diff ecadlib/eval/golden`, commit.")
|
|
442
|
+
return rc
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
if __name__ == "__main__":
|
|
446
|
+
sys.exit(main())
|
|
Binary file
|