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/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)