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/firstrun.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""First-run conveniences for a new user (used by `ecadlib init`, `ecadlib demo` and ecadlib/cli.py):
|
|
2
|
+
|
|
3
|
+
- setup questions for `ecadlib init`: which tool (Altium / KiCad / both), your library folders, the workspace folder,
|
|
4
|
+
which part sources to use, and which API keys are set (variable NAMES only, never values). The answers go in
|
|
5
|
+
ecadlib.toml [setup] (and [sources] order); `--yes` takes the defaults without asking. Afterwards `init` runs the
|
|
6
|
+
doctor and prints the next commands to run.
|
|
7
|
+
- `ecadlib demo`: a small library built from a bundled sample BOM, fully offline (standard parts only, no keys), in a
|
|
8
|
+
real workspace folder, so `ecadlib status` and `ecadlib ui` can open it.
|
|
9
|
+
- checks the CLI runs before a command: --lib folders that do not exist and output folders that cannot be written
|
|
10
|
+
give one line and a fix instead of a traceback or a silent empty result; [setup] libs / workspace fill in --lib /
|
|
11
|
+
--workspace when neither the command line nor ECADLIB_LIB / ECADLIB_WORKSPACE give them.
|
|
12
|
+
- `blockers(rows)`: after a librarian run, one line per thing that held parts back for a reason outside the part
|
|
13
|
+
(network blocked, no API key for the datasheet reader), with the fix."""
|
|
14
|
+
import argparse, json, os, re, shutil, sys, tempfile, time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import rules as RU
|
|
18
|
+
|
|
19
|
+
TOOLS = ("altium", "kicad", "both")
|
|
20
|
+
DEMO_BOM = Path(__file__).with_name("assets") / "demo_bom.csv"
|
|
21
|
+
DEMO_NAME = "ecadlib_demo"
|
|
22
|
+
DEMO_HEADER = "# ecadlib demo workspace (written by `ecadlib demo`)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------ setup questions (ecadlib init)
|
|
26
|
+
def key_status(rules=None, environ=None):
|
|
27
|
+
"""[(env var name, set?, what it unlocks)] - names only; values are never read beyond 'is it non-empty'."""
|
|
28
|
+
from .doctor import key_services
|
|
29
|
+
environ = os.environ if environ is None else environ
|
|
30
|
+
out = []
|
|
31
|
+
for name, env, why in key_services(rules):
|
|
32
|
+
ok = all(environ.get(e) for e in env)
|
|
33
|
+
if name == "altium365" and not ok and environ.get("NEXAR_CLIENT_ID") and environ.get("NEXAR_CLIENT_SECRET"):
|
|
34
|
+
env, ok = ["NEXAR_CLIENT_ID", "NEXAR_CLIENT_SECRET"], True
|
|
35
|
+
out.append((" + ".join(env), ok, why))
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _split(text):
|
|
40
|
+
return [p.strip() for p in re.split(r"[,;]", text or "") if p.strip()]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _yes(ans, default=True):
|
|
44
|
+
ans = (ans or "").strip().lower()
|
|
45
|
+
return default if not ans else ans in ("y", "yes", "1", "true")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ask_setup(existing=None, given=None, inp=input, out=print, interactive=True, rules=None, environ=None):
|
|
49
|
+
"""The [setup] answers {tool, libs, workspace, sources}. existing: the current [setup]/[sources] of ecadlib.toml
|
|
50
|
+
(its values are the defaults); given: values from the command line (never asked). interactive=False: no questions,
|
|
51
|
+
defaults only. sources is the [sources] order to write, or None to leave it as it is."""
|
|
52
|
+
existing, given = existing or {}, {k: v for k, v in (given or {}).items() if v not in (None, [], "")}
|
|
53
|
+
st = dict(existing.get("setup") or {})
|
|
54
|
+
cur_sources = list((existing.get("sources") or {}).get("order") or RU.DEFAULTS["sources"]["order"])
|
|
55
|
+
ans = {"tool": given.get("tool") or st.get("tool") or "both",
|
|
56
|
+
"libs": list(given.get("libs") or st.get("libs") or []),
|
|
57
|
+
"workspace": given.get("workspace") or st.get("workspace") or "ecadlib_workspace",
|
|
58
|
+
"sources": list(given["sources"]) if given.get("sources") else None}
|
|
59
|
+
keys = key_status(rules, environ)
|
|
60
|
+
ds_env = ((rules or {}).get("datasheet") or {}).get("api_key_env") or "ANTHROPIC_API_KEY"
|
|
61
|
+
ds_key = next((ok for n, ok, _ in keys if n == ds_env), False)
|
|
62
|
+
if interactive:
|
|
63
|
+
out("\nSetup (Enter keeps the value in [brackets]):")
|
|
64
|
+
if "tool" not in given:
|
|
65
|
+
for _ in range(3):
|
|
66
|
+
a = inp(f" Which tool do you make libraries for? altium / kicad / both [{ans['tool']}]: ").strip().lower()
|
|
67
|
+
if not a or a in TOOLS:
|
|
68
|
+
ans["tool"] = a or ans["tool"]
|
|
69
|
+
break
|
|
70
|
+
out(" please type altium, kicad or both")
|
|
71
|
+
else:
|
|
72
|
+
out(f" keeping {ans['tool']}")
|
|
73
|
+
if "libs" not in given:
|
|
74
|
+
a = inp(" Your existing library folders (KiCad .kicad_sym/.pretty or Altium .SchLib/.PcbLib), separated by commas"
|
|
75
|
+
f" [{', '.join(ans['libs']) or 'none'}]: ").strip()
|
|
76
|
+
if a:
|
|
77
|
+
ans["libs"] = [] if a.lower() == "none" else _split(a)
|
|
78
|
+
for l in ans["libs"]:
|
|
79
|
+
if not Path(l).expanduser().exists():
|
|
80
|
+
out(f" note: {l} does not exist yet; check the path (the setup check will flag it too)")
|
|
81
|
+
if "workspace" not in given:
|
|
82
|
+
a = inp(f" Folder where ecadlib keeps your BOMs and the libraries it builds [{ans['workspace']}]: ").strip()
|
|
83
|
+
ans["workspace"] = a or ans["workspace"]
|
|
84
|
+
if ans["sources"] is None:
|
|
85
|
+
online = _yes(inp(" Look missing parts up online (KiCad library on gitlab.com, EasyEDA/LCSC)? [Y/n]: "))
|
|
86
|
+
ds = _yes(inp(f" Let a model read the datasheet for parts no library has? (needs {ds_env}, which is "
|
|
87
|
+
f"{'set' if ds_key else 'not set yet'}) [{'Y/n' if ds_key else 'y/N'}]: "), default=ds_key)
|
|
88
|
+
order = [s for s in cur_sources if (online or s not in ("kicad", "easyeda")) and (ds or s != "datasheet")]
|
|
89
|
+
if "standard" not in order:
|
|
90
|
+
order.append("standard")
|
|
91
|
+
ans["sources"] = order if order != cur_sources else None
|
|
92
|
+
return ans
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def setup_raw(ans):
|
|
96
|
+
"""The rules-file sections for the answers (added to what `init` infers)."""
|
|
97
|
+
raw = {"setup": {"tool": ans["tool"], "libs": [str(l) for l in ans["libs"]], "workspace": str(ans["workspace"])}}
|
|
98
|
+
if ans.get("sources"):
|
|
99
|
+
raw["sources"] = {"order": list(ans["sources"])}
|
|
100
|
+
return raw
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def keys_lines(rules=None, environ=None):
|
|
104
|
+
"""One line on the API keys that matter for a first run (names only); the doctor lists every one."""
|
|
105
|
+
main = [k for k in key_status(rules, environ) if not k[2].endswith("part lookups")]
|
|
106
|
+
have = [n for n, ok, _ in main if ok]
|
|
107
|
+
miss = [f"{n} ({why.split(',')[0]})" for n, ok, why in main if not ok]
|
|
108
|
+
return ["API keys (names only; `ecadlib keys set NAME`, the setup page or your environment, never ecadlib.toml): "
|
|
109
|
+
+ "; ".join(x for x in (f"set: {', '.join(have)}" if have else "", f"not set: {', '.join(miss)}" if miss else "") if x)
|
|
110
|
+
+ ". None is needed for KiCad-library and standard parts."]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def next_steps(ans, rules_file="ecadlib.toml"):
|
|
114
|
+
"""[(command, what it does)]: the 2-3 commands to run after `ecadlib init`."""
|
|
115
|
+
ws = ans.get("workspace") or "ecadlib_workspace"
|
|
116
|
+
lib = f"{ws}/librarian/YOUR_BOM" # <workspace>/librarian/*/plan.json: `ecadlib status` lists it
|
|
117
|
+
steps = [("ecadlib demo", "see a real result first: a small library from a bundled BOM, offline, in under a minute"),
|
|
118
|
+
(f"ecadlib librarian YOUR_BOM.csv -o {lib}", "fill every missing symbol and footprint for your BOM, then the "
|
|
119
|
+
"release check (`ecadlib status` shows the result)")]
|
|
120
|
+
if ans.get("tool") in ("kicad", "both"):
|
|
121
|
+
steps.append((f"ecadlib kicad-export {lib}/plan.json -o {ws}/kicad", "the library as KiCad files with fp-lib-table / sym-lib-table"))
|
|
122
|
+
if ans.get("tool") in ("altium", "both"):
|
|
123
|
+
steps.append(("ecadlib ui", f"open the web UI on {ws}: review held parts, sign off, send to Altium (DbLib, .SchLib/.PcbLib, Altium 365)"))
|
|
124
|
+
return steps
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def finish_init(ans, rules_file, out=print, environ=None, run_doctor=True):
|
|
128
|
+
"""After `init` wrote the file: make the workspace folder, run the doctor, print the next commands. Returns the
|
|
129
|
+
doctor report (None when not run)."""
|
|
130
|
+
from . import doctor
|
|
131
|
+
ws = Path(ans["workspace"]).expanduser()
|
|
132
|
+
if not ws.is_absolute():
|
|
133
|
+
ws = Path(rules_file).resolve().parent / ws
|
|
134
|
+
try:
|
|
135
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
except OSError as e:
|
|
137
|
+
out(f"could not create the workspace {ws}: {e.strerror or e}")
|
|
138
|
+
rules = RU.load(str(rules_file))
|
|
139
|
+
for line in keys_lines(rules, environ):
|
|
140
|
+
out(line)
|
|
141
|
+
rep = None
|
|
142
|
+
if run_doctor:
|
|
143
|
+
libs = [str(_rel_to(l, rules_file)) for l in ans["libs"]]
|
|
144
|
+
rep = doctor.run(str(ws), libs, str(rules_file), environ)
|
|
145
|
+
out("\nChecking the setup (ecadlib doctor):")
|
|
146
|
+
from . import term as TM # styled on a terminal; plain when captured (web UI, tests)
|
|
147
|
+
out(doctor.render(rep, TM.wants_style(sys.stdout), TM.term_width(sys.stdout), TM.VERBOSITY) if out is print
|
|
148
|
+
else doctor.render(rep))
|
|
149
|
+
out("\nNext:")
|
|
150
|
+
for c, why in next_steps(ans):
|
|
151
|
+
out(f" {c}\n {why}")
|
|
152
|
+
return rep
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _rel_to(p, rules_file):
|
|
156
|
+
p = Path(str(p)).expanduser()
|
|
157
|
+
return p if p.is_absolute() else (Path(rules_file).resolve().parent / p)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------ [setup] defaults for --lib / --workspace
|
|
161
|
+
def config_defaults(cmd, argv, environ=None, start="."):
|
|
162
|
+
"""argv with --lib / --workspace from ecadlib.toml [setup] added where the command takes them (the same commands
|
|
163
|
+
as ECADLIB_LIB / ECADLIB_WORKSPACE) and neither the command line nor the environment gives them. Relative paths
|
|
164
|
+
in [setup] start at the rules file's folder. Returns (argv, notes). Never raises: a bad file is left to the command."""
|
|
165
|
+
from .cliutil import ENV_FLAGS, _has_flag, _subcommand
|
|
166
|
+
environ = os.environ if environ is None else environ
|
|
167
|
+
argv, notes = list(argv), []
|
|
168
|
+
if any(a in ("-h", "--help") for a in argv):
|
|
169
|
+
return argv, notes
|
|
170
|
+
wanted = [(var, flag, cmds[cmd]) for var, (flag, cmds) in ENV_FLAGS.items()
|
|
171
|
+
if cmd in cmds and not (environ.get(var) or "").strip() and not _has_flag(argv, flag)]
|
|
172
|
+
if not wanted:
|
|
173
|
+
return argv, notes
|
|
174
|
+
try:
|
|
175
|
+
f = RU.find(start)
|
|
176
|
+
if not f:
|
|
177
|
+
return argv, notes
|
|
178
|
+
raw = RU.parse_text(f.read_text(encoding="utf-8"), f.name)
|
|
179
|
+
st = raw.get("setup") or {}
|
|
180
|
+
except Exception:
|
|
181
|
+
return argv, notes
|
|
182
|
+
for var, flag, subs in wanted:
|
|
183
|
+
vals = st.get("libs") if flag == "--lib" else [st.get("workspace")] if st.get("workspace") else []
|
|
184
|
+
vals = [v for v in (vals or []) if isinstance(v, str) and v.strip()]
|
|
185
|
+
if not vals:
|
|
186
|
+
continue
|
|
187
|
+
extra = [x for v in vals for x in (flag, str(_rel_to(v, f)))]
|
|
188
|
+
if subs is None:
|
|
189
|
+
argv = extra + argv
|
|
190
|
+
elif _subcommand(argv) in subs:
|
|
191
|
+
argv = argv + extra
|
|
192
|
+
else:
|
|
193
|
+
continue
|
|
194
|
+
notes.append(f"{flag} from [setup] in {f}")
|
|
195
|
+
return argv, notes
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ------------------------------------------------------------------ checks before a command runs
|
|
199
|
+
LIB_CHECKED = {"triage", "librarian", "resolve", "kicad-export", "eagle-export", "easyeda-export", "mcp", "ui", "agent-watch",
|
|
200
|
+
"agent", "dedupe"}
|
|
201
|
+
OUT_CHECKED = {"init", "demo", "triage", "librarian", "kicad-export", "eagle-export", "easyeda-export", "kicad-dblib", "dblib",
|
|
202
|
+
"export-parts", "dblib-sync"}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _values(argv, flags):
|
|
206
|
+
out, it = [], iter(argv)
|
|
207
|
+
for a in it:
|
|
208
|
+
if a == "--":
|
|
209
|
+
break
|
|
210
|
+
for f in flags:
|
|
211
|
+
if a == f:
|
|
212
|
+
v = next(it, None)
|
|
213
|
+
if v is not None:
|
|
214
|
+
out.append(v)
|
|
215
|
+
elif f.startswith("--") and a.startswith(f + "="):
|
|
216
|
+
out.append(a.split("=", 1)[1])
|
|
217
|
+
elif not f.startswith("--") and a.startswith(f) and len(a) > len(f) and not a.startswith("--"):
|
|
218
|
+
out.append(a[len(f):])
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def writable_problem(path):
|
|
223
|
+
"""None when `path` (a file or folder to be created) can be written, else a short reason."""
|
|
224
|
+
p = Path(path).expanduser()
|
|
225
|
+
q = p
|
|
226
|
+
while not q.exists():
|
|
227
|
+
if q.parent == q:
|
|
228
|
+
break
|
|
229
|
+
q = q.parent
|
|
230
|
+
if q.exists() and not q.is_dir():
|
|
231
|
+
if q == p:
|
|
232
|
+
q = q.parent
|
|
233
|
+
else:
|
|
234
|
+
return f"{q} is a file, not a folder"
|
|
235
|
+
try:
|
|
236
|
+
with tempfile.TemporaryFile(dir=q, prefix=".ecadlib-probe-"):
|
|
237
|
+
pass
|
|
238
|
+
except OSError as e:
|
|
239
|
+
return f"cannot write in {q} ({e.strerror or e})"
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def preflight(cmd, argv, environ=None):
|
|
244
|
+
"""(message, hint) for a command line that cannot work (a --lib folder that does not exist, an output folder that
|
|
245
|
+
cannot be written), or None. Runs before the command, so a typo gives one line instead of a silent empty result."""
|
|
246
|
+
if any(a in ("-h", "--help") for a in argv):
|
|
247
|
+
return None
|
|
248
|
+
if cmd in LIB_CHECKED:
|
|
249
|
+
for l in _values(argv, ["--lib"]):
|
|
250
|
+
if not Path(l).expanduser().exists():
|
|
251
|
+
return (f"library folder not found: {l}",
|
|
252
|
+
"fix --lib (or ECADLIB_LIB, or [setup] libs in ecadlib.toml); `ecadlib doctor` checks every path")
|
|
253
|
+
if cmd in OUT_CHECKED:
|
|
254
|
+
flags = ["--to"] if cmd == "dblib-sync" else ["-o", "--out"]
|
|
255
|
+
for o in _values(argv, flags + (["--out"] if cmd == "dblib-sync" else [])):
|
|
256
|
+
why = writable_problem(o)
|
|
257
|
+
if why:
|
|
258
|
+
return (f"cannot write the output {o}: {why}", "pick another -o / --out folder you can write to")
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------ after a librarian run
|
|
263
|
+
def bom_hint(err):
|
|
264
|
+
"""A fix line for a BOM the reader could not use ('' when there is nothing to add)."""
|
|
265
|
+
t = str(err)
|
|
266
|
+
if "no BOM header found" in t:
|
|
267
|
+
return ("\n hint: name a column MPN (or Manufacturer Part Number), Designator or Value; or map your headers in "
|
|
268
|
+
"ecadlib.toml, e.g. [bom.columns] mpn = \"Part No\" (`ecadlib triage BOM` shows what was recognised)")
|
|
269
|
+
if "the BOM file is empty" in t or "no part rows" in t:
|
|
270
|
+
return "\n hint: export the BOM again with its part rows (one row per part or per group of designators)"
|
|
271
|
+
return ""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
NET_RE = re.compile(r"(ProxyError|ConnectionError|ConnectTimeout|ReadTimeout|timed out|Name or service not known|"
|
|
275
|
+
r"Temporary failure in name resolution|proxy CONNECT refused|HTTP 403|HTTP 407|HTTP 429|"
|
|
276
|
+
r"Max retries exceeded|offline and not (?:cached|downloaded))", re.I)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def blockers(rows, rules=None, offline=False):
|
|
280
|
+
"""[(message, fix)] for causes outside the parts that held them back: part sources that could not be reached, and
|
|
281
|
+
the datasheet reader without an API key. rows: librarian.review_rows(items) (each has `part` and `tried`)."""
|
|
282
|
+
net, nokey = {}, []
|
|
283
|
+
for r in rows:
|
|
284
|
+
for step in re.split(r";\s+(?=[a-z_]+[: ])", r.get("tried") or ""):
|
|
285
|
+
src = step.split(":", 1)[0].split(" ", 1)[0].strip()
|
|
286
|
+
m = NET_RE.search(step)
|
|
287
|
+
if m and src:
|
|
288
|
+
net.setdefault(src, [set(), m.group(0)])[0].add(r["part"])
|
|
289
|
+
if "no API key" in step:
|
|
290
|
+
nokey.append(r["part"])
|
|
291
|
+
out = []
|
|
292
|
+
for src, (parts, why) in sorted(net.items()):
|
|
293
|
+
if offline:
|
|
294
|
+
out.append((f"{src}: not in the download cache for {len(parts)} part(s) (--offline)",
|
|
295
|
+
"run once without --offline to fill the cache"))
|
|
296
|
+
else:
|
|
297
|
+
out.append((f"network: {src} could not be reached (or refused) for {len(parts)} part(s) ({why})",
|
|
298
|
+
"check your internet connection or proxy, or skip online lookups: --offline --sources standard"))
|
|
299
|
+
if nokey:
|
|
300
|
+
env = ((rules or {}).get("datasheet") or {}).get("api_key_env") or "ANTHROPIC_API_KEY"
|
|
301
|
+
out.append((f"{len(set(nokey))} part(s) need the datasheet reader, but {env} is not set",
|
|
302
|
+
f"add it on the setup page (`ecadlib setup`) or with `ecadlib keys set {env}`, or take datasheet out of "
|
|
303
|
+
"[sources] order in ecadlib.toml"))
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ------------------------------------------------------------------ ecadlib demo
|
|
308
|
+
def build_demo(ws, tool="both", bom=None, log=print):
|
|
309
|
+
"""Build the demo library in Workspace `ws`, offline, from the bundled BOM (or `bom`). Returns a summary dict."""
|
|
310
|
+
from .app import Workspace
|
|
311
|
+
t0 = time.time()
|
|
312
|
+
root = Path(ws.root)
|
|
313
|
+
if root.resolve() == Path(root.resolve().anchor):
|
|
314
|
+
raise ValueError(f"refusing to build the demo at the filesystem root ({root}); give a folder, e.g. -o ecadlib-demo")
|
|
315
|
+
rules_f = root / "ecadlib.toml"
|
|
316
|
+
if rules_f.exists() and not rules_f.read_text(encoding="utf-8").startswith(DEMO_HEADER):
|
|
317
|
+
raise ValueError(f"{root} has its own ecadlib.toml; the demo needs a folder of its own (e.g. -o ecadlib-demo)")
|
|
318
|
+
if not rules_f.exists(): # the demo's own rules: standard parts only, so a stray ecadlib.toml in a
|
|
319
|
+
rules_f.write_text( # parent folder cannot change the result, and nothing goes on the network
|
|
320
|
+
DEMO_HEADER + ": standard parts only, no network, no API keys.\n"
|
|
321
|
+
"[sources]\norder = [\"standard\"]\n\n[setup]\n"
|
|
322
|
+
f"tool = \"{tool}\"\nlibs = []\nworkspace = \".\"\n", encoding="utf-8")
|
|
323
|
+
src = Path(bom) if bom else DEMO_BOM
|
|
324
|
+
dest = root / "uploads" / "demo" / src.name
|
|
325
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
if not dest.exists() or dest.read_bytes() != src.read_bytes():
|
|
327
|
+
shutil.copyfile(src, dest)
|
|
328
|
+
rel = ws.rel(dest)
|
|
329
|
+
log(f"[1/3] BOM: {rel} ({sum(1 for _ in open(dest, encoding='utf-8')) - 1} lines)")
|
|
330
|
+
log("[2/3] finding symbols and footprints (built-in standard parts; no network, no API key) ...")
|
|
331
|
+
res = ws.librarian_run(rel, sources=["standard"])
|
|
332
|
+
files = {"plan": res["reports"]}
|
|
333
|
+
kicad = None
|
|
334
|
+
if tool in ("kicad", "both"):
|
|
335
|
+
log("[3/3] writing KiCad files (.kicad_sym, .pretty, lib tables, database library) ...")
|
|
336
|
+
kicad = ws.kicad_dblib(rel, name=DEMO_NAME)
|
|
337
|
+
else:
|
|
338
|
+
log("[3/3] Altium files are written by step 2 (.SchLib / .PcbLib)")
|
|
339
|
+
lib_dir = ws._librarian_dir(dest)
|
|
340
|
+
par = ws.parity(rel, kicad=bool(kicad), name=DEMO_NAME) if tool in ("kicad", "both") else None # both libraries agree
|
|
341
|
+
if par:
|
|
342
|
+
log(f" KiCad / Altium parity: {par['counts']['match']} parts match, {par['counts']['mismatch']} differ ({par['report']})")
|
|
343
|
+
altium = sorted(ws.rel(p) for p in (lib_dir / "models").glob("*/*") if p.suffix in (".SchLib", ".PcbLib"))
|
|
344
|
+
return {"workspace": str(root), "bom": rel, "parts": res["parts"], "ready": res["ready"], "held": res["held"],
|
|
345
|
+
"held_parts": [r["part"] for r in res["review"] if r["status"] != "ready"],
|
|
346
|
+
"review": [r for r in res["reports"] if r.endswith("review.md")],
|
|
347
|
+
"plan": ws.rel(lib_dir / "plan.json"), "altium": altium,
|
|
348
|
+
"kicad": kicad and {"folder": kicad["folder"], "files": kicad["files"]},
|
|
349
|
+
"parity": par and {"ok": par["ok"], "counts": par["counts"], "report": par["report"], "mismatches": par["mismatches"]},
|
|
350
|
+
"seconds": round(time.time() - t0, 1)}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def demo_main(argv=None):
|
|
354
|
+
ap = argparse.ArgumentParser(
|
|
355
|
+
prog="ecadlib demo", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
356
|
+
description="build a small library from a bundled sample BOM, fully offline (no network, no API keys), "
|
|
357
|
+
"so you can see a real result in under a minute",
|
|
358
|
+
epilog="examples:\n ecadlib demo # writes ./ecadlib-demo\n"
|
|
359
|
+
" ecadlib demo -o /tmp/try --tool kicad\n ecadlib demo --bom my_bom.csv # your BOM, standard parts only")
|
|
360
|
+
ap.add_argument("-o", "--out", "--workspace", dest="out", default="ecadlib-demo",
|
|
361
|
+
help="workspace folder to build in (default %(default)s); re-running reuses it")
|
|
362
|
+
ap.add_argument("--tool", choices=TOOLS, default=None, help="which files to write (default: [setup] tool, else both)")
|
|
363
|
+
ap.add_argument("--bom", help="use this BOM instead of the bundled sample (still offline, standard parts only)")
|
|
364
|
+
ap.add_argument("--json", action="store_true", help="print the summary as JSON")
|
|
365
|
+
a = ap.parse_args(argv)
|
|
366
|
+
from .app import Workspace
|
|
367
|
+
tool = a.tool
|
|
368
|
+
if tool is None:
|
|
369
|
+
try:
|
|
370
|
+
tool = (RU.load().get("setup") or {}).get("tool") or "both"
|
|
371
|
+
except RU.RulesError:
|
|
372
|
+
tool = "both"
|
|
373
|
+
say = (lambda *x: None) if a.json else print
|
|
374
|
+
say(f"ecadlib demo: building a small library in {a.out}/ from "
|
|
375
|
+
f"{'the bundled sample BOM' if not a.bom else a.bom}, offline")
|
|
376
|
+
ws = Workspace(a.out, offline=True)
|
|
377
|
+
try:
|
|
378
|
+
res = build_demo(ws, tool, a.bom, log=say)
|
|
379
|
+
except ValueError as e: # a folder with someone's rules, or a BOM the reader cannot use
|
|
380
|
+
print(f"ecadlib demo: {e}" + bom_hint(e), file=sys.stderr)
|
|
381
|
+
return 2
|
|
382
|
+
if a.json:
|
|
383
|
+
print(json.dumps(res, indent=1))
|
|
384
|
+
return 0
|
|
385
|
+
from . import term as TM
|
|
386
|
+
t = TM.Term()
|
|
387
|
+
written = [(f"{a.out}/{res['plan']}", "the library plan"), (", ".join(f"{a.out}/{r}" for r in res["review"]), "review: what to check, part by part")]
|
|
388
|
+
if res["altium"]:
|
|
389
|
+
written.append((f"{a.out}/{Path(res['altium'][0]).parent.parent}/", f"{len(res['altium'])} Altium files (.SchLib, .PcbLib)"))
|
|
390
|
+
if res["kicad"]:
|
|
391
|
+
kd = Path(res["workspace"]) / res["kicad"]["folder"]
|
|
392
|
+
written.append((f"{a.out}/{res['kicad']['folder']}/", "KiCad: " + ", ".join(sorted(p.name + ("/" if p.is_dir() else "")
|
|
393
|
+
for p in kd.iterdir() if p.suffix in (".kicad_sym", ".pretty", ".kicad_dbl")))))
|
|
394
|
+
t.summary(f"{res['parts']} parts: {res['ready']} ready, {res['held']} held in {res['seconds']} s"
|
|
395
|
+
+ (f" (held: {', '.join(res['held_parts'])}; that is expected, see the review)" if res["held"] else ""),
|
|
396
|
+
written=written,
|
|
397
|
+
next=[(f"ecadlib status --workspace {a.out}", "one-screen summary"),
|
|
398
|
+
(f"ecadlib ui --workspace {a.out}", "look at it in the browser (held parts, sign-off, exports)"),
|
|
399
|
+
("ecadlib init", "set up for your own libraries and BOMs")])
|
|
400
|
+
return 0
|