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/altium_export.py
ADDED
|
@@ -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())
|