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/sexpr.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Minimal S-expression reader/writer for KiCad files (.kicad_mod / .kicad_sym).
|
|
2
|
+
|
|
3
|
+
Only parses; never evaluates. Safe for untrusted input (bounded by file size). parse(text, keep_quotes=True)
|
|
4
|
+
returns quoted strings as Q (a str subclass) so dumps() can write a tree back with the same quoting."""
|
|
5
|
+
import math, re
|
|
6
|
+
|
|
7
|
+
_TOKEN = re.compile(r'\s*(?:(\()|(\))|"((?:[^"\\]|\\.)*)"|([^\s()"]+))', re.S)
|
|
8
|
+
_ESC = {"n": "\n", "r": "\r", "t": "\t"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Q(str):
|
|
12
|
+
"""A string that was (or must be written) quoted."""
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def quote(s):
|
|
17
|
+
"""KiCad string literal: backslash, double quote and line breaks escaped."""
|
|
18
|
+
return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + '"'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse(text: str, keep_quotes=False):
|
|
22
|
+
"""Return nested lists. Quoted strings -> str (Q with keep_quotes), bare atoms -> str (numbers kept as text)."""
|
|
23
|
+
stack, cur = [], []
|
|
24
|
+
pos, end = 0, len(text)
|
|
25
|
+
while pos < end:
|
|
26
|
+
m = _TOKEN.match(text, pos)
|
|
27
|
+
if not m:
|
|
28
|
+
if text[pos:].strip():
|
|
29
|
+
bad = text[pos:].lstrip()[:20]
|
|
30
|
+
raise ValueError(f"unterminated string or stray character near {bad!r}")
|
|
31
|
+
break
|
|
32
|
+
pos = m.end()
|
|
33
|
+
if m.group(1):
|
|
34
|
+
stack.append(cur)
|
|
35
|
+
cur = []
|
|
36
|
+
elif m.group(2):
|
|
37
|
+
if not stack:
|
|
38
|
+
raise ValueError("unbalanced ')'")
|
|
39
|
+
parent = stack.pop()
|
|
40
|
+
parent.append(cur)
|
|
41
|
+
cur = parent
|
|
42
|
+
elif m.group(3) is not None:
|
|
43
|
+
v = re.sub(r"\\(.)", lambda e: _ESC.get(e.group(1), e.group(1)), m.group(3), flags=re.S)
|
|
44
|
+
cur.append(Q(v) if keep_quotes else v)
|
|
45
|
+
else:
|
|
46
|
+
cur.append(m.group(4))
|
|
47
|
+
if stack:
|
|
48
|
+
raise ValueError("unbalanced '(' - %d unclosed" % len(stack))
|
|
49
|
+
return cur[0] if len(cur) == 1 else cur
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_INLINE = {"pts", "at", "xyz", "font", "effects", "stroke", "fill", "layers", "size", "offset", "scale", "rotate", "justify"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def dumps(node, indent=0, width=100):
|
|
56
|
+
"""Write a tree back as KiCad-style text (tab indented). Q atoms (and empty strings) are quoted, other atoms
|
|
57
|
+
written bare; a list of atoms, or a short (at ...) / (effects ...) / (pts ...) style list, stays on one line."""
|
|
58
|
+
atom = lambda a: quote(a) if isinstance(a, Q) or a == "" else str(a)
|
|
59
|
+
if not isinstance(node, list):
|
|
60
|
+
return atom(node)
|
|
61
|
+
flat = "(" + " ".join(dumps(c, 0, 10 ** 9) if isinstance(c, list) else atom(c) for c in node) + ")"
|
|
62
|
+
if all(not isinstance(c, list) for c in node) or (node and node[0] in _INLINE and len(flat) <= width):
|
|
63
|
+
return flat
|
|
64
|
+
pad = "\t" * (indent + 1)
|
|
65
|
+
head, rest = [], list(node)
|
|
66
|
+
while rest and not isinstance(rest[0], list): # leading atoms stay on the first line
|
|
67
|
+
head.append(atom(rest.pop(0)))
|
|
68
|
+
out = [pad + (dumps(c, indent + 1, width) if isinstance(c, list) else atom(c)) for c in rest]
|
|
69
|
+
return "(" + " ".join(head) + "\n" + "\n".join(out) + "\n" + "\t" * indent + ")"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find(node, head):
|
|
73
|
+
"""Yield direct children lists whose first element == head."""
|
|
74
|
+
for c in node:
|
|
75
|
+
if isinstance(c, list) and c and c[0] == head:
|
|
76
|
+
yield c
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def first(node, head, default=None):
|
|
80
|
+
return next(find(node, head), default)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pads(mod_tree):
|
|
84
|
+
"""Extract pads from a parsed footprint: list of dicts (number, shape, x, y, w, h, layers)."""
|
|
85
|
+
out = []
|
|
86
|
+
for p in find(mod_tree, "pad"):
|
|
87
|
+
at, size = first(p, "at"), first(p, "size")
|
|
88
|
+
out.append({"number": p[1], "type": p[2], "shape": p[3],
|
|
89
|
+
"x": float(at[1]), "y": float(at[2]),
|
|
90
|
+
"w": float(size[1]), "h": float(size[2]),
|
|
91
|
+
"layers": (first(p, "layers") or ["layers"])[1:]})
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _arc_extent(s, m, e):
|
|
96
|
+
"""Points bounding a circular arc start -> mid -> end: its ends plus every axis extreme the sweep passes."""
|
|
97
|
+
(ax, ay), (bx, by), (cx, cy) = s, m, e
|
|
98
|
+
d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
|
|
99
|
+
if abs(d) < 1e-12: # collinear: a straight segment
|
|
100
|
+
return [s, m, e]
|
|
101
|
+
ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d
|
|
102
|
+
uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d
|
|
103
|
+
r = math.hypot(ax - ux, ay - uy)
|
|
104
|
+
ang = lambda x, y: math.atan2(y - uy, x - ux) % (2 * math.pi)
|
|
105
|
+
a0, am, a1 = ang(ax, ay), ang(bx, by), ang(cx, cy)
|
|
106
|
+
ccw = (am - a0) % (2 * math.pi) < (a1 - a0) % (2 * math.pi) # the sweep that passes through mid
|
|
107
|
+
lo, sweep = (a0, (a1 - a0) % (2 * math.pi)) if ccw else (a1, (a0 - a1) % (2 * math.pi))
|
|
108
|
+
pts = [s, e]
|
|
109
|
+
for k in range(4):
|
|
110
|
+
t = k * math.pi / 2
|
|
111
|
+
if (t - lo) % (2 * math.pi) <= sweep + 1e-12:
|
|
112
|
+
pts.append((ux + r * math.cos(t), uy + r * math.sin(t)))
|
|
113
|
+
return pts
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def courtyard_bbox(mod_tree):
|
|
117
|
+
"""Bounding box of the F.CrtYd graphics: lines, rectangles, polygons, circles and arcs (their true extent)."""
|
|
118
|
+
xs, ys = [], []
|
|
119
|
+
add = lambda p: (xs.append(float(p[0])), ys.append(float(p[1])))
|
|
120
|
+
for kind in ("fp_line", "fp_rect", "fp_poly", "fp_circle", "fp_arc"):
|
|
121
|
+
for g in find(mod_tree, kind):
|
|
122
|
+
if (first(g, "layer") or [None, None])[1] != "F.CrtYd":
|
|
123
|
+
continue
|
|
124
|
+
pt = lambda k: (float(first(g, k)[1]), float(first(g, k)[2])) if first(g, k) else None
|
|
125
|
+
if kind == "fp_circle" and pt("center") and pt("end"):
|
|
126
|
+
(cx, cy), (ex, ey) = pt("center"), pt("end")
|
|
127
|
+
r = math.hypot(ex - cx, ey - cy)
|
|
128
|
+
add((cx - r, cy - r)); add((cx + r, cy + r))
|
|
129
|
+
continue
|
|
130
|
+
if kind == "fp_arc" and pt("start") and pt("end"):
|
|
131
|
+
if pt("mid"):
|
|
132
|
+
for p in _arc_extent(pt("start"), pt("mid"), pt("end")):
|
|
133
|
+
add(p)
|
|
134
|
+
continue
|
|
135
|
+
if first(g, "angle"): # KiCad 5 arcs: start = centre, end = first point, angle in degrees
|
|
136
|
+
(cx, cy), (ex, ey) = pt("start"), pt("end")
|
|
137
|
+
a = math.radians(float(first(g, "angle")[1]))
|
|
138
|
+
rot = lambda t: (cx + (ex - cx) * math.cos(t) - (ey - cy) * math.sin(t),
|
|
139
|
+
cy + (ex - cx) * math.sin(t) + (ey - cy) * math.cos(t))
|
|
140
|
+
for p in _arc_extent((ex, ey), rot(a / 2), rot(a)):
|
|
141
|
+
add(p)
|
|
142
|
+
continue
|
|
143
|
+
for k in ("start", "end"):
|
|
144
|
+
p = first(g, k)
|
|
145
|
+
if p:
|
|
146
|
+
add((p[1], p[2]))
|
|
147
|
+
for xy in find(first(g, "pts") or [], "xy"):
|
|
148
|
+
add((xy[1], xy[2]))
|
|
149
|
+
return (min(xs), min(ys), max(xs), max(ys)) if xs else None
|
ecadlib/sheetsafe.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Spreadsheet-safe report cells (CSV formula injection, OWASP): a cell that starts with = + - @ tab or CR is run as a
|
|
2
|
+
formula by Excel / LibreOffice / Google Sheets, so a BOM cell like =HYPERLINK(...) or =cmd|' /C calc'!A0 would run
|
|
3
|
+
when someone opens a report. Such cells get a leading apostrophe (Excel's "this is text" mark). Plain numbers
|
|
4
|
+
(-5, +3.3, -1.5e-3, 12%) and a lone '-' stay as they are. bom_triage.read_bom strips the apostrophe again, so a BOM
|
|
5
|
+
ecadlib writes reads back unchanged.
|
|
6
|
+
|
|
7
|
+
The DbLib's data files keep Altium's '=Value' / '=Part Number' Comment literal: the .xlsx stores every cell as text
|
|
8
|
+
(no spreadsheet evaluates it), and the .csv mirror uses param_refs=True (bare names only; '=HYPERLINK(..)' is still
|
|
9
|
+
neutralised)."""
|
|
10
|
+
import csv, re
|
|
11
|
+
|
|
12
|
+
_RISKY = re.compile(r"^[=+\-@\t\r]")
|
|
13
|
+
_NUMBER = re.compile(r"^[-+]?(\d+([.,]\d*)?|[.,]\d+)([eE][-+]?\d+)?%?$")
|
|
14
|
+
_PARAM_REF = re.compile(r"^=[A-Za-z][A-Za-z0-9 _]*$") # Altium's '=Value' / '=Part Number': names only, no calls
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cell(v, param_refs=False):
|
|
18
|
+
"""The value to write into a report cell. param_refs: keep Altium '=Name' parameter references literal (the DbLib
|
|
19
|
+
data files); they hold no function call or operator, so nothing can run."""
|
|
20
|
+
if (isinstance(v, str) and _RISKY.match(v) and v not in ("-", "+") and not _NUMBER.match(v.strip())
|
|
21
|
+
and not (param_refs and _PARAM_REF.match(v))):
|
|
22
|
+
return "'" + v
|
|
23
|
+
return v
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def unquote(v):
|
|
27
|
+
"""Undo cell() when reading a BOM back: "'=X" -> "=X" (only for the cells cell() would have changed)."""
|
|
28
|
+
if isinstance(v, str) and v.startswith("'") and _RISKY.match(v[1:]) and cell(v[1:]) == v:
|
|
29
|
+
return v[1:]
|
|
30
|
+
return v
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DictWriter(csv.DictWriter):
|
|
34
|
+
def __init__(self, *a, param_refs=False, **k):
|
|
35
|
+
super().__init__(*a, **k)
|
|
36
|
+
self.param_refs = param_refs
|
|
37
|
+
|
|
38
|
+
def writerow(self, rowdict):
|
|
39
|
+
return super().writerow({k: cell(v, self.param_refs) for k, v in rowdict.items()})
|
|
40
|
+
|
|
41
|
+
def writerows(self, rowdicts):
|
|
42
|
+
for r in rowdicts:
|
|
43
|
+
self.writerow(r)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Writer:
|
|
47
|
+
def __init__(self, w):
|
|
48
|
+
self._w = w
|
|
49
|
+
|
|
50
|
+
def writerow(self, row):
|
|
51
|
+
return self._w.writerow([cell(v) for v in row])
|
|
52
|
+
|
|
53
|
+
def writerows(self, rows):
|
|
54
|
+
for r in rows:
|
|
55
|
+
self.writerow(r)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def writer(f, *a, **k):
|
|
59
|
+
"""csv.writer whose cells go through cell()."""
|
|
60
|
+
return _Writer(csv.writer(f, *a, **k))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def frame(df):
|
|
64
|
+
"""A pandas DataFrame with every cell through cell() (for DataFrame.to_csv)."""
|
|
65
|
+
return df.map(cell) if hasattr(df, "map") else df.applymap(cell)
|