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
@@ -0,0 +1,209 @@
1
+ """Altium Database Library (DbLib) export prototype.
2
+
3
+ Altium .SchLib/.PcbLib are binary OLE compound files, so instead of writing them we emit
4
+ the parts *database* Altium reads through a .DbLib file (OLE DB / ODBC):
5
+
6
+ out/altium/parts.sqlite one table per category (SQLite; needs the SQLite3 ODBC driver on Windows)
7
+ out/altium/parts.xlsx same tables as sheets (Excel via the ACE OLE DB provider; tables are 'Sheet$')
8
+ out/altium/ecadlib_sqlite.DbLib, ecadlib_excel.DbLib INI files pointing Altium at the above
9
+
10
+ python -m ecadlib.altium_export examples/sample_bom.csv -o out/altium [--triage out/triage_sample_bom.csv]
11
+
12
+ Column names use Altium's reserved DbLib names ([Library Ref], [Library Path], [Footprint Ref],
13
+ [Footprint Path], [Description]) plus parameters (Part Number = key, Value, Comment, Manufacturer,
14
+ Manufacturer Part Number, Supplier 1/Supplier Part Number 1, ComponentLink1URL/Description).
15
+ See README for what is verified vs assumed about the .DbLib INI keys."""
16
+ import argparse, re, sqlite3, sys
17
+ from contextlib import closing
18
+ from pathlib import Path, PureWindowsPath
19
+
20
+ import pandas as pd
21
+
22
+ from .altium_lib import file_stem, lib_name
23
+ from .bom_triage import read_bom
24
+ from .kicad_index import package_of
25
+
26
+ COLUMNS = ["Part Number", "Library Ref", "Library Path", "Footprint Ref", "Footprint Path",
27
+ "Description", "Value", "Comment", "Manufacturer", "Manufacturer Part Number",
28
+ "Supplier 1", "Supplier Part Number 1", "Supplier 2", "Supplier Part Number 2",
29
+ "ComponentLink1Description", "ComponentLink1URL", "ComponentLink2Description", "ComponentLink2URL"]
30
+ RESERVED = {"Library Ref", "Library Path", "Footprint Ref", "Footprint Path", "Description"}
31
+ CATEGORY = {"R": "Resistors", "C": "Capacitors", "L": "Inductors", "D": "Diodes", "Q": "Transistors", # whole prefix:
32
+ "Y": "Crystals", "J": "Connectors", "P": "Connectors", "U": "ICs", # LED1 is not an L,
33
+ "LED": "Diodes", "IC": "ICs", "CN": "Connectors", "JP": "Connectors"} # CN1 not a C
34
+ GENERIC_SYMBOL = {"Resistors": "Resistor", "Capacitors": "Capacitor", "Inductors": "Inductor"}
35
+
36
+
37
+ def to_rows(lines, triage=None, rules=None):
38
+ """{category: DataFrame of DbLib rows}. rules (ecadlib.rules): part names, footprint names, value style and
39
+ supplier columns; None keeps ecadlib's own. df.attrs["kicad_fp"] maps a part to its footprint's original name
40
+ when the rules renamed it."""
41
+ from . import rules as RU
42
+ custom = rules is not None and not RU.is_default(rules, "naming")
43
+ tri = {}
44
+ if triage is not None:
45
+ for _, t in triage.iterrows():
46
+ if str(t["mpn"]): # an empty MPN would hand one row's footprint to every MPN-less line
47
+ tri[str(t["mpn"])] = t
48
+ out, seen, renamed = {}, {}, {}
49
+ for ln in lines:
50
+ prefix = re.match(r"[A-Z]+", ln.get("designator", "").upper() or "U")
51
+ cat = CATEGORY.get(prefix.group(0) if prefix else "U", "Misc")
52
+ mpn, lcsc = ln.get("mpn", ""), ln.get("lcsc", "")
53
+ fp = ln.get("footprint", "")
54
+ t = tri.get(mpn)
55
+ if t is not None and isinstance(t.get("local_footprint"), str) and t["local_footprint"]:
56
+ fp = t["local_footprint"].split(" ")[0]
57
+ fp_ref = fp.split(":")[-1]
58
+ pkg = package_of(fp, ln.get("description")) or ""
59
+ value = ln.get("value", "")
60
+ if custom:
61
+ f = RU.line_fields(ln, cat, pkg, rules, prefix.group(0) if prefix else "U")
62
+ pn = (RU.component_name(f, rules) or ln.get("designator", "").split(",")[0]).replace(" ", "")
63
+ else:
64
+ pn = (f"{cat[:3].upper()}-{pkg}-{value}" if cat in GENERIC_SYMBOL and value
65
+ else mpn or value or ln.get("designator", "").split(",")[0]).replace(" ", "")
66
+ if seen.setdefault(pn, mpn) != mpn: # same synthetic PN, different part (e.g. 100nF 16V vs 50V): keep both
67
+ pn = f"{pn}-{mpn}".replace(" ", "")
68
+ lib_ref = lib_name(GENERIC_SYMBOL.get(cat) or (custom and mpn and RU.symbol_name(f, rules)) or mpn or pn)
69
+ kicad_fp = fp_ref = lib_name(fp_ref)
70
+ if custom:
71
+ fp_ref = lib_name(RU.footprint_name(fp_ref, rules, prefix.group(0) if prefix else None))
72
+ if fp_ref != kicad_fp:
73
+ renamed[pn] = kicad_fp
74
+ out.setdefault(cat, []).append({
75
+ "Part Number": pn,
76
+ "Library Ref": lib_ref,
77
+ "Library Path": f"SchLib\\{file_stem(lib_ref)}.SchLib", # altium_lib layout: one entry per file
78
+ "Footprint Ref": fp_ref,
79
+ "Footprint Path": f"PcbLib\\{file_stem(fp_ref)}.PcbLib" if fp_ref else "",
80
+ "Description": ln.get("description", ""),
81
+ "Value": value, "Comment": "=Value" if cat in GENERIC_SYMBOL else mpn,
82
+ "Manufacturer": ln.get("manufacturer", ""), "Manufacturer Part Number": mpn,
83
+ "Supplier 1": "LCSC" if lcsc else "", "Supplier Part Number 1": lcsc,
84
+ "Supplier 2": "", "Supplier Part Number 2": "",
85
+ "ComponentLink1Description": "LCSC" if lcsc else "",
86
+ "ComponentLink1URL": f"https://www.lcsc.com/product-detail/{lcsc}.html" if lcsc else "",
87
+ "ComponentLink2Description": "Datasheet" if lcsc else "",
88
+ "ComponentLink2URL": f"https://www.lcsc.com/datasheet/{lcsc}.pdf" if lcsc else "",
89
+ })
90
+ cols = columns(rules)
91
+ res = {k: pd.DataFrame(v, columns=cols).fillna("").drop_duplicates("Part Number") for k, v in sorted(out.items())}
92
+ for df in res.values():
93
+ df.attrs["kicad_fp"] = renamed
94
+ return res
95
+
96
+
97
+ def columns(rules=None):
98
+ """COLUMNS with the rules' number of supplier slots (2 = COLUMNS as is)."""
99
+ n = (rules or {}).get("parameters", {}).get("supplier_slots", 2)
100
+ sup = [c for i in range(1, n + 1) for c in (f"Supplier {i}", f"Supplier Part Number {i}")]
101
+ return COLUMNS[:10] + sup + COLUMNS[14:] if n != 2 else list(COLUMNS)
102
+
103
+
104
+ def sqlite_connection(db_path):
105
+ return ('Provider=MSDASQL.1;Persist Security Info=False;'
106
+ f'Extended Properties="DRIVER=SQLite3 ODBC Driver;Database={db_path};LongNames=0;Timeout=1000;NoTXN=0;SyncPragma=NORMAL;StepAPI=0;"')
107
+
108
+
109
+ def excel_connection(xlsx_path):
110
+ return (f'Provider=Microsoft.ACE.OLEDB.12.0;Data Source={xlsx_path};'
111
+ 'Extended Properties="Excel 12.0 Xml;HDR=YES;IMEX=1";Persist Security Info=False')
112
+
113
+
114
+ def dblib_links(conn_str, last_table, left_quote="[", right_quote="]", db_type="", db_path="", relative=False,
115
+ search_path="", extra=()):
116
+ """[OutputDatabaseLinkFile] + [DatabaseLinks] lines. db_type/db_path/relative are what Altium's DbLib editor
117
+ writes for its Access/Excel fast connection (LibraryDatabaseType=Microsoft Excel, LibraryDatabasePath=x.xls,
118
+ DatabasePathRelative=1); extra = (key, value) pairs appended to [DatabaseLinks] (SVNRepositoryURL ...)."""
119
+ return (["[OutputDatabaseLinkFile]", "Version=1.1", "[DatabaseLinks]", f"ConnectionString={conn_str}",
120
+ "AddMode=3", "RemoveMode=1", "UpdateMode=2", "ViewMode=0",
121
+ f"LeftQuote={left_quote}", f"RightQuote={right_quote}", "QuoteTableNames=1", "UseTableSchemaName=0",
122
+ "DefaultColumnType=VARCHAR(255)", f"LibraryDatabaseType={db_type}", f"LibraryDatabasePath={db_path}",
123
+ f"DatabasePathRelative={int(relative)}", "TopPanelCollapsed=0", f"LibrarySearchPath={search_path}",
124
+ "OrcadMultiValueDelimiter=,", "SearchSubDirectories=0", "SchemaName=", f"LastFocusedTable={last_table}"]
125
+ + [f"{k}={v}" for k, v in extra])
126
+
127
+
128
+ def table_section(i, table):
129
+ return [f"[Table{i}]", "SchemaName=", f"TableName={table}", "Enabled=True", "UserWhere=0", "UserWhereText="]
130
+
131
+
132
+ def fieldmap_section(i, table, column, key="Part Number"):
133
+ ftype = 0 if column == key else 1 # observed: key column FieldType=0
134
+ pname = f"[{column}]" if column in RESERVED else column
135
+ return [f"[FieldMap{i}]",
136
+ f"Options=FieldName={table}.{column}|TableNameOnly={table}|FieldNameOnly={column}|FieldType={ftype}|"
137
+ f"ParameterName={pname}|VisibleOnAdd=False|AddMode=0|RemoveMode=0|UpdateMode=0"]
138
+
139
+
140
+ def dblib_text(conn_str, tables, left_quote="[", right_quote="]", excel=False, columns=COLUMNS, **links):
141
+ """INI layout copied from real-world .DbLib files (see README 'verified'). tables: table names, or
142
+ {table: columns}; excel adds the '$' Altium's Excel provider puts after sheet names."""
143
+ cols = tables if isinstance(tables, dict) else {t: columns for t in tables}
144
+ names = [t + ("$" if excel else "") for t in cols]
145
+ L = dblib_links(conn_str, names[0], left_quote, right_quote, **links)
146
+ for i, tn in enumerate(names, 1):
147
+ L += table_section(i, tn)
148
+ fm = 0
149
+ for tn, cs in zip(names, cols.values()):
150
+ for c in cs:
151
+ fm += 1
152
+ L += fieldmap_section(fm, tn, c)
153
+ return "\r\n".join(L) + "\r\n"
154
+
155
+
156
+ def main(argv=None):
157
+ ap = argparse.ArgumentParser(description="Write an Altium DbLib (SQLite + XLSX) from a BOM")
158
+ ap.add_argument("bom")
159
+ ap.add_argument("-o", "--out", default="out/altium")
160
+ ap.add_argument("--triage", help="triage CSV from bom_triage (to pick up resolved local footprints)")
161
+ ap.add_argument("--win-dir", default=r"C:\ecadlib\altium", help="Windows folder the DB will live in (for the connection string)")
162
+ ap.add_argument("--rules", help="library rules file (default: ecadlib.toml in the output folder, the current folder or a parent)")
163
+ a = ap.parse_args(argv)
164
+
165
+ from . import rules as RU
166
+ rules = RU.load(a.rules, start=a.out)
167
+ try:
168
+ lines, _ = read_bom(a.bom, RU.bom_columns(rules), rules)
169
+ except (ValueError, OSError) as e: # unreadable / headerless BOM: a message, not a traceback
170
+ print(f"cannot read BOM: {e}", file=sys.stderr)
171
+ return 2
172
+ if not lines:
173
+ print(f"cannot read BOM: {a.bom} has a header but no part lines", file=sys.stderr)
174
+ return 2
175
+ tri = pd.read_csv(a.triage, dtype=str).fillna("") if a.triage else None
176
+ tables = to_rows(lines, tri, rules)
177
+ tables = {k: df.rename(columns=lambda c: RU.house_row({c: 0}, rules).popitem()[0]) for k, df in tables.items()}
178
+ out = Path(a.out); out.mkdir(parents=True, exist_ok=True)
179
+
180
+ db = out / "parts.sqlite"
181
+ db.unlink(missing_ok=True)
182
+ with closing(sqlite3.connect(db)) as con, con: # `with con` alone commits but never closes
183
+ for name, df in tables.items():
184
+ cols = ", ".join(f'"{c}" TEXT' + (" PRIMARY KEY" if c == "Part Number" else "") for c in df.columns)
185
+ con.execute(f'CREATE TABLE "{name}" ({cols})')
186
+ con.executemany(f'INSERT INTO "{name}" VALUES ({",".join("?" * len(df.columns))})', df.values.tolist())
187
+ with pd.ExcelWriter(out / "parts.xlsx", engine="openpyxl") as w: # a declared dependency
188
+ for name, df in tables.items():
189
+ df.to_excel(w, sheet_name=name, index=False)
190
+ for row in w.sheets[name].iter_rows(min_row=2): # all text: Comment "=Value" is Altium's parameter
191
+ for c in row: # reference, not an Excel formula (#NAME?)
192
+ if isinstance(c.value, str):
193
+ c.data_type = "s"
194
+
195
+ win = PureWindowsPath(a.win_dir)
196
+ sqlite_cs, excel_cs = sqlite_connection(win / "parts.sqlite"), excel_connection(win / "parts.xlsx")
197
+ names = {k: list(df.columns) for k, df in tables.items()}
198
+ (out / "ecadlib_sqlite.DbLib").write_text(dblib_text(sqlite_cs, names, '"', '"'), newline="")
199
+ # the Excel one names its workbook relative to itself (DatabasePathRelative=1, as Altium's DbLib editor writes it),
200
+ # so the folder can move; the SQLite ODBC string has no relative form and keeps --win-dir
201
+ (out / "ecadlib_excel.DbLib").write_text(dblib_text(excel_cs, names, "[", "]", excel=True, db_type="Microsoft Excel",
202
+ db_path="parts.xlsx", relative=True), newline="")
203
+ for n_, df in tables.items():
204
+ print(f"{n_:<12} {len(df):>2} parts e.g. {df.iloc[0]['Part Number']} -> {df.iloc[0]['Footprint Ref']}")
205
+ print(f"wrote {db}, {out/'parts.xlsx'}, {out/'ecadlib_sqlite.DbLib'}, {out/'ecadlib_excel.DbLib'}")
206
+
207
+
208
+ if __name__ == "__main__":
209
+ sys.exit(main())