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/completion.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""`ecadlib completion bash|zsh|fish`: a shell completion script generated from the commands' own argparse parsers,
|
|
2
|
+
so it always matches this version (commands, subcommands, options, fixed choices).
|
|
3
|
+
|
|
4
|
+
ecadlib completion bash > ~/.local/share/bash-completion/completions/ecadlib
|
|
5
|
+
ecadlib completion zsh > "${fpath[1]}/_ecadlib" (or: source <(ecadlib completion zsh))
|
|
6
|
+
ecadlib completion fish > ~/.config/fish/completions/ecadlib.fish
|
|
7
|
+
|
|
8
|
+
Each command's parser is captured by running its main() with parse_args replaced by a stub that stops at once, so
|
|
9
|
+
nothing is read, written or fetched. A command whose parser cannot be captured still completes by name."""
|
|
10
|
+
import argparse, contextlib, importlib, io, sys
|
|
11
|
+
|
|
12
|
+
GLOBAL = ["-h", "--help", "-V", "--version", "--debug"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Got(Exception):
|
|
16
|
+
def __init__(self, parser):
|
|
17
|
+
self.parser = parser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _capture(target):
|
|
21
|
+
"""The ArgumentParser `target` ("module" or "module:function") builds in its main(), or None."""
|
|
22
|
+
mod, _, fn = target.partition(":")
|
|
23
|
+
orig = (argparse.ArgumentParser.parse_args, argparse.ArgumentParser.parse_known_args)
|
|
24
|
+
|
|
25
|
+
def stop(self, *a, **k):
|
|
26
|
+
raise _Got(self)
|
|
27
|
+
argparse.ArgumentParser.parse_args = argparse.ArgumentParser.parse_known_args = stop
|
|
28
|
+
try:
|
|
29
|
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
30
|
+
getattr(importlib.import_module(mod), fn or "main")([])
|
|
31
|
+
except _Got as g:
|
|
32
|
+
return g.parser
|
|
33
|
+
except BaseException: # no argparse, or main() needs more: complete the name only
|
|
34
|
+
return None
|
|
35
|
+
finally:
|
|
36
|
+
argparse.ArgumentParser.parse_args, argparse.ArgumentParser.parse_known_args = orig
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def describe(parser):
|
|
41
|
+
"""{"options": [...], "choices": {option: [...]}, "subcommands": {name: describe(sub)}, "positional_choices": [...]}"""
|
|
42
|
+
out = {"options": [], "value_options": [], "choices": {}, "subcommands": {}, "positional_choices": []}
|
|
43
|
+
if parser is None:
|
|
44
|
+
return out
|
|
45
|
+
for act in parser._actions:
|
|
46
|
+
if isinstance(act, argparse._SubParsersAction):
|
|
47
|
+
for name, sub in act.choices.items():
|
|
48
|
+
out["subcommands"][name] = describe(sub)
|
|
49
|
+
elif act.option_strings:
|
|
50
|
+
names = [o for o in act.option_strings if o.startswith("--") or len(o) == 2]
|
|
51
|
+
out["options"] += names
|
|
52
|
+
if act.nargs != 0:
|
|
53
|
+
out["value_options"] += names
|
|
54
|
+
if act.choices:
|
|
55
|
+
for o in act.option_strings:
|
|
56
|
+
out["choices"][o] = [str(c) for c in act.choices]
|
|
57
|
+
elif act.choices:
|
|
58
|
+
out["positional_choices"] += [str(c) for c in act.choices]
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tree(commands=None):
|
|
63
|
+
from .cli import COMMANDS
|
|
64
|
+
commands = COMMANDS if commands is None else commands
|
|
65
|
+
out = {}
|
|
66
|
+
for name, (target, help_) in commands.items():
|
|
67
|
+
d = describe(_capture(target)) if name != "completion" else describe(None)
|
|
68
|
+
if name == "completion":
|
|
69
|
+
d["positional_choices"] = list(SHELLS)
|
|
70
|
+
d["help"] = help_
|
|
71
|
+
out[name] = d
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _words(xs):
|
|
76
|
+
return " ".join(sorted(set(xs)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def bash(t):
|
|
80
|
+
L = ["# bash completion for ecadlib (generated by `ecadlib completion bash`)",
|
|
81
|
+
"_ecadlib() {",
|
|
82
|
+
' local cur prev cmd ci sub i',
|
|
83
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
84
|
+
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
85
|
+
' cmd=""; ci=0; sub=""',
|
|
86
|
+
' for ((i=1; i<COMP_CWORD; i++)); do',
|
|
87
|
+
' case "${COMP_WORDS[i]}" in -*) continue;; esac',
|
|
88
|
+
' cmd="${COMP_WORDS[i]}"; ci=$i; break',
|
|
89
|
+
' done',
|
|
90
|
+
' if [[ -z "$cmd" ]]; then',
|
|
91
|
+
f' COMPREPLY=( $(compgen -W "{_words(list(t) + GLOBAL)}" -- "$cur") ); return 0',
|
|
92
|
+
' fi',
|
|
93
|
+
' local opts="" choices=""',
|
|
94
|
+
' case "$cmd" in']
|
|
95
|
+
for name, d in t.items():
|
|
96
|
+
L.append(f" {name})")
|
|
97
|
+
if d["subcommands"]:
|
|
98
|
+
names = "|".join(sorted(d["subcommands"]))
|
|
99
|
+
L.append(f' for ((i=ci+1; i<COMP_CWORD; i++)); do case "${{COMP_WORDS[i]}}" in {names}) sub="${{COMP_WORDS[i]}}"; break;; esac; done')
|
|
100
|
+
for opt, ch in d["choices"].items():
|
|
101
|
+
L.append(f' [[ "$prev" == "{opt}" ]] && {{ COMPREPLY=( $(compgen -W "{_words(ch)}" -- "$cur") ); return 0; }}')
|
|
102
|
+
if d["subcommands"]:
|
|
103
|
+
L.append(' case "$sub" in')
|
|
104
|
+
for sn, sd in d["subcommands"].items():
|
|
105
|
+
L.append(f" {sn})")
|
|
106
|
+
for opt, ch in sd["choices"].items():
|
|
107
|
+
L.append(f' [[ "$prev" == "{opt}" ]] && {{ COMPREPLY=( $(compgen -W "{_words(ch)}" -- "$cur") ); return 0; }}')
|
|
108
|
+
L.append(f' opts="{_words(sd["options"] + d["options"])}"; choices="{_words(sd["positional_choices"])}";;')
|
|
109
|
+
L.append(f' *) opts="{_words(d["options"])}"; choices="{_words(list(d["subcommands"]) + d["positional_choices"])}";;')
|
|
110
|
+
L.append(" esac;;")
|
|
111
|
+
else:
|
|
112
|
+
L.append(f' opts="{_words(d["options"])}"; choices="{_words(d["positional_choices"])}";;')
|
|
113
|
+
L += [' esac',
|
|
114
|
+
' if [[ "$cur" == -* ]]; then',
|
|
115
|
+
' COMPREPLY=( $(compgen -W "$opts --debug" -- "$cur") )',
|
|
116
|
+
' elif [[ -n "$choices" ]]; then',
|
|
117
|
+
' COMPREPLY=( $(compgen -W "$choices" -- "$cur") )',
|
|
118
|
+
' else',
|
|
119
|
+
' COMPREPLY=( $(compgen -f -- "$cur") )',
|
|
120
|
+
' fi',
|
|
121
|
+
' return 0',
|
|
122
|
+
'}',
|
|
123
|
+
'complete -o filenames -o bashdefault -F _ecadlib ecadlib']
|
|
124
|
+
return "\n".join(L) + "\n"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def zsh(t):
|
|
128
|
+
"""zsh runs the bash function through bashcompinit (ships with zsh), so both shells complete the same way."""
|
|
129
|
+
return ("#compdef ecadlib\n# zsh completion for ecadlib (generated by `ecadlib completion zsh`)\n"
|
|
130
|
+
"autoload -U +X bashcompinit && bashcompinit\n" + bash(t).split("\n", 1)[1])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _fq(s):
|
|
134
|
+
return "'" + str(s).replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def fish(t):
|
|
138
|
+
L = ["# fish completion for ecadlib (generated by `ecadlib completion fish`)",
|
|
139
|
+
"complete -c ecadlib -f",
|
|
140
|
+
"complete -c ecadlib -l debug -d 'show tracebacks'",
|
|
141
|
+
"complete -c ecadlib -n __fish_use_subcommand -s V -l version -d 'print the version'"]
|
|
142
|
+
for name, d in t.items():
|
|
143
|
+
L.append(f"complete -c ecadlib -n __fish_use_subcommand -a {name} -d {_fq(d['help'][:80])}")
|
|
144
|
+
cond = f"'__fish_seen_subcommand_from {name}'"
|
|
145
|
+
if d["subcommands"]:
|
|
146
|
+
subs = " ".join(d["subcommands"])
|
|
147
|
+
L.append(f"complete -c ecadlib -n \"__fish_seen_subcommand_from {name}; and not __fish_seen_subcommand_from {subs}\" "
|
|
148
|
+
f"-a {_fq(subs)}")
|
|
149
|
+
if d["positional_choices"]:
|
|
150
|
+
L.append(f"complete -c ecadlib -n {cond} -a {_fq(' '.join(d['positional_choices']))}")
|
|
151
|
+
opts = set(d["options"]) | {o for sd in d["subcommands"].values() for o in sd["options"]}
|
|
152
|
+
for o in sorted(opts):
|
|
153
|
+
flag = f"-l {o[2:]}" if o.startswith("--") else f"-s {o[1:]}"
|
|
154
|
+
ch = d["choices"].get(o) or next((sd["choices"][o] for sd in d["subcommands"].values() if o in sd["choices"]), None)
|
|
155
|
+
takes = o in d["value_options"] or any(o in sd["value_options"] for sd in d["subcommands"].values())
|
|
156
|
+
L.append(f"complete -c ecadlib -n {cond} {flag}" + (f" -x -a {_fq(' '.join(ch))}" if ch else " -r -F" if takes else ""))
|
|
157
|
+
L.append(f"complete -c ecadlib -n {cond} -F")
|
|
158
|
+
return "\n".join(L) + "\n"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
SHELLS = {"bash": bash, "zsh": zsh, "fish": fish}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv=None):
|
|
165
|
+
ap = argparse.ArgumentParser(prog="ecadlib completion", description="print a shell completion script")
|
|
166
|
+
ap.add_argument("shell", choices=list(SHELLS))
|
|
167
|
+
a = ap.parse_args(argv)
|
|
168
|
+
sys.stdout.write(SHELLS[a.shell](tree()))
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
sys.exit(main())
|
ecadlib/costs.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""What the AI features cost, per job, for your model or for every model we know a price for.
|
|
2
|
+
|
|
3
|
+
ecadlib costs per-job estimate for the model in your rules ([ai] / [datasheet])
|
|
4
|
+
ecadlib costs --model gpt-6-luna the same for another model ("local:..." for an [ai.endpoints] entry)
|
|
5
|
+
ecadlib costs --all [--md F] [--csv F] [--xlsx F] every model in the sheet (docs/costs.md is made with --md)
|
|
6
|
+
|
|
7
|
+
Only two things call a model: a datasheet read (a part no library, EasyEDA or standard package covers) and the
|
|
8
|
+
optional visual check ([agent.proof]). Everything else (KiCad libraries, EasyEDA, stdparts, lifecycle, stock,
|
|
9
|
+
upstream checks) is free. These are estimates from the request sizes the code builds, not a bill:
|
|
10
|
+
|
|
11
|
+
datasheet read [datasheet] page_budget pages x datasheet.PAGE_TOKENS + the prompt + the output schema in,
|
|
12
|
+
datasheet.OUT_TOKENS out (Claude synchronous: input billed as a 1.25x cache write)
|
|
13
|
+
visual check ([agent.proof] max_pages + 2 drawings of ours) x visual_check.IMG_TOKENS_MAX in, 3000 out
|
|
14
|
+
50-line BOM SHARE of the lines need a datasheet (the rest resolve free)
|
|
15
|
+
500-part library the same share of 500 parts
|
|
16
|
+
|
|
17
|
+
Retries cost more: a read that comes back incomplete is re-sent with more pages or (route = "ladder") on the
|
|
18
|
+
next model; `ecadlib datasheet estimate BOM` gives the worst case of a real BOM. Prices: datasheet.PRICES for
|
|
19
|
+
Claude, ecadlib.llm.profiles.PRICES for the others, $0 for a model on your own computer."""
|
|
20
|
+
import argparse, csv, json, sys
|
|
21
|
+
|
|
22
|
+
from . import datasheet as D
|
|
23
|
+
|
|
24
|
+
SHARE = 0.2 # lines of a typical BOM that need a datasheet (the bundled sample: 3 of 14 held without one)
|
|
25
|
+
BOM_LINES, LIBRARY_PARTS = 50, 500
|
|
26
|
+
PROOF_OUT = 3000
|
|
27
|
+
# The sheet: (label, model id, provider). Local models are free whatever they are.
|
|
28
|
+
SHEET = [
|
|
29
|
+
("Claude Fable 5.1", "claude-fable-5-1", "anthropic"),
|
|
30
|
+
("Claude Opus 5.5 (default)", "claude-opus-5-5", "anthropic"),
|
|
31
|
+
("Claude Sonnet 5.5", "claude-sonnet-5-5", "anthropic"),
|
|
32
|
+
("Claude Haiku 5.5", "claude-haiku-5-5", "anthropic"),
|
|
33
|
+
("OpenAI GPT-6 Astra", "gpt-6-astra", "openai"),
|
|
34
|
+
("OpenAI GPT-6.1 Sol", "gpt-6.1-sol", "openai"),
|
|
35
|
+
("OpenAI GPT-5.6 Terra", "gpt-5.6-terra", "openai"),
|
|
36
|
+
("OpenAI GPT-5.4 mini", "gpt-5.4-mini", "openai"),
|
|
37
|
+
("OpenAI GPT-6 Luna", "gpt-6-luna", "openai"),
|
|
38
|
+
("OpenAI GPT-5 nano", "gpt-5-nano", "openai"),
|
|
39
|
+
("Google Gemini 4 Argon", "gemini-4-argon", "gemini"),
|
|
40
|
+
("Google Gemini 3.1 Pro", "gemini-3.1-pro-preview", "gemini"),
|
|
41
|
+
("Google Gemini 3.8 Flash", "gemini-3.8-flash", "gemini"),
|
|
42
|
+
("Google Gemini 3.1 Flash-Lite", "gemini-3.1-flash-lite", "gemini"),
|
|
43
|
+
("DeepSeek V4 Pro", "deepseek-v4-pro", "openai-compatible"),
|
|
44
|
+
("DeepSeek V4.1 Flash", "deepseek-flash", "openai-compatible"),
|
|
45
|
+
("Local model (Ollama, LM Studio, vLLM)", "local", "ollama"),
|
|
46
|
+
]
|
|
47
|
+
COLS = ["model", "id", "tier", "usd_in", "usd_out", "read", "read_batch", "check", "bom50", "bom50_checked",
|
|
48
|
+
"lib500", "lib500_batch", "lib500_check_all"]
|
|
49
|
+
HEAD = {"model": "Model", "id": "Model id", "tier": "Tier", "usd_in": "$ in / MTok", "usd_out": "$ out / MTok",
|
|
50
|
+
"read": "Datasheet read", "read_batch": "Read as batch", "check": "Visual check", "bom50": "50-line BOM",
|
|
51
|
+
"bom50_checked": "50-line BOM + checks", "lib500": "500-part library", "lib500_batch": "500-part library, batch",
|
|
52
|
+
"lib500_check_all": "Visual check on all 500"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_tokens(page_budget=None):
|
|
56
|
+
"""(input, output) tokens of one datasheet read at the default page budget."""
|
|
57
|
+
pages = page_budget or D.SETTINGS["page_budget"]
|
|
58
|
+
return pages * D.PAGE_TOKENS + 1500 + len(json.dumps(D.output_schema())) // 4, D.OUT_TOKENS
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_tokens(max_pages=None):
|
|
62
|
+
from . import visual_check as V
|
|
63
|
+
pages = max_pages or V.PROOF_DEFAULTS["max_pages"]
|
|
64
|
+
return (pages + 2) * V.IMG_TOKENS_MAX + (len(V.SYSTEM) + len(json.dumps(V.output_schema()))) // 3, PROOF_OUT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def price(model, provider="anthropic", rules=None):
|
|
68
|
+
"""(input, output) $ per MTok, or None: Claude's table, the profile's price, $0 for a local server."""
|
|
69
|
+
if model == "local" or provider in ("ollama", "lmstudio", "vllm", "llamacpp"):
|
|
70
|
+
return 0.0, 0.0
|
|
71
|
+
if model in D.PRICES:
|
|
72
|
+
return D.PRICES[model]
|
|
73
|
+
from .llm import profiles as P
|
|
74
|
+
p = P.profile(model, "openai-compatible" if provider != "anthropic" else provider, rules)
|
|
75
|
+
if p.get("price_in") is None or p.get("price_out") is None:
|
|
76
|
+
return None
|
|
77
|
+
return float(p["price_in"]), float(p["price_out"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def jobs(model, provider="anthropic", rules=None, share=SHARE, vision=None):
|
|
81
|
+
"""One row of the sheet: USD per job (None where it does not apply or the price is unknown)."""
|
|
82
|
+
from .llm import profiles as P
|
|
83
|
+
claude = provider == "anthropic"
|
|
84
|
+
prof = P.profile(model, provider if claude else "openai-compatible", rules) if model != "local" else {}
|
|
85
|
+
row = {"id": model, "tier": prof.get("tier", "depends"), "usd_in": None, "usd_out": None}
|
|
86
|
+
pr = price(model, provider, rules)
|
|
87
|
+
if not pr:
|
|
88
|
+
return dict(row, **{k: None for k in COLS if k not in row})
|
|
89
|
+
pin, pout = pr
|
|
90
|
+
rin, rout = read_tokens()
|
|
91
|
+
cin, cout = check_tokens()
|
|
92
|
+
read = (rin * (D.CACHE_WRITE if claude else 1.0) * pin + rout * pout) / 1e6
|
|
93
|
+
batch = (rin * pin + rout * pout) / 1e6 * D.BATCH_DISCOUNT if claude else None # Message Batches: Claude only
|
|
94
|
+
sees = prof.get("vision", True) if vision is None else vision
|
|
95
|
+
check = (cin * pin + cout * pout) / 1e6 if sees else None
|
|
96
|
+
n_bom, n_lib = round(BOM_LINES * share), round(LIBRARY_PARTS * share)
|
|
97
|
+
return dict(row, usd_in=pin, usd_out=pout, read=read, read_batch=batch, check=check, bom50=n_bom * read,
|
|
98
|
+
bom50_checked=n_bom * (read + check) if check is not None else None, lib500=n_lib * read,
|
|
99
|
+
lib500_batch=n_lib * batch if batch is not None else None,
|
|
100
|
+
lib500_check_all=LIBRARY_PARTS * check if check is not None else None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def sheet(rules=None, share=SHARE):
|
|
104
|
+
return [dict(jobs(mid, prov, rules, share), model=label) for label, mid, prov in SHEET]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def money(v):
|
|
108
|
+
if v is None:
|
|
109
|
+
return "n/a"
|
|
110
|
+
if v == 0:
|
|
111
|
+
return "$0"
|
|
112
|
+
return f"${v:,.4f}" if v < 0.01 else f"${v:,.3f}" if v < 1 else f"${v:,.2f}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def markdown(rows, share=SHARE):
|
|
116
|
+
rin, rout = read_tokens()
|
|
117
|
+
cin, cout = check_tokens()
|
|
118
|
+
cols = ["model", "usd_in", "usd_out", "read", "read_batch", "check", "bom50", "bom50_checked", "lib500",
|
|
119
|
+
"lib500_batch", "lib500_check_all"]
|
|
120
|
+
out = ["| " + " | ".join(HEAD[c] for c in cols) + " |", "|" + "---|" * len(cols)]
|
|
121
|
+
for r in rows:
|
|
122
|
+
cells = []
|
|
123
|
+
for c in cols:
|
|
124
|
+
v = r.get(c)
|
|
125
|
+
cells.append(str(v) if c == "model" else ("" if v is None else f"${v:g}") if c in ("usd_in", "usd_out")
|
|
126
|
+
else money(v))
|
|
127
|
+
out.append("| " + " | ".join(cells) + " |")
|
|
128
|
+
return "\n".join(out), (rin, rout, cin, cout, round(BOM_LINES * share), round(LIBRARY_PARTS * share))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def write_csv(rows, path):
|
|
132
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
133
|
+
w = csv.writer(f)
|
|
134
|
+
w.writerow([HEAD.get(c, c) + (" (USD)" if c not in ("model", "id", "tier", "usd_in", "usd_out") else "")
|
|
135
|
+
for c in COLS])
|
|
136
|
+
for r in rows:
|
|
137
|
+
w.writerow(["" if r.get(c) is None else (round(r[c], 4) if isinstance(r[c], float) else r[c]) for c in COLS])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def write_xlsx(rows, path, share=SHARE):
|
|
141
|
+
from openpyxl import Workbook
|
|
142
|
+
from openpyxl.styles import Font
|
|
143
|
+
wb = Workbook()
|
|
144
|
+
ws = wb.active
|
|
145
|
+
ws.title = "Cost per job"
|
|
146
|
+
ws.append([HEAD[c] for c in COLS])
|
|
147
|
+
for c in ws[1]:
|
|
148
|
+
c.font = Font(bold=True)
|
|
149
|
+
for r in rows:
|
|
150
|
+
ws.append([r.get(c) for c in COLS])
|
|
151
|
+
for row in ws.iter_rows(min_row=2, min_col=COLS.index("read") + 1):
|
|
152
|
+
for c in row:
|
|
153
|
+
c.number_format = '"$"#,##0.000'
|
|
154
|
+
for i, c in enumerate(COLS, 1):
|
|
155
|
+
ws.column_dimensions[ws.cell(1, i).column_letter].width = 30 if c == "model" else 16
|
|
156
|
+
_, (rin, rout, cin, cout, nb, nl) = markdown(rows, share)
|
|
157
|
+
n = wb.create_sheet("How it is estimated")
|
|
158
|
+
for line in [
|
|
159
|
+
"Estimates, not a bill: request sizes from the code, list prices checked 2026-10-16.",
|
|
160
|
+
f"Datasheet read: ~{rin:,} tokens in, ~{rout:,} out. Visual check: ~{cin:,} in, ~{cout:,} out.",
|
|
161
|
+
f"50-line BOM: {nb} lines need a datasheet ({share:.0%}); 500-part library: {nl} parts. The rest cost $0.",
|
|
162
|
+
"Claude synchronous reads bill input as a 1.25x cache write; Message Batches (Claude only) are half price.",
|
|
163
|
+
"Retries (more pages, or the next model on the ladder) add to this; `ecadlib datasheet estimate BOM` gives the worst case.",
|
|
164
|
+
"Other models are called through ecadlib.llm (openai-compatible): no batch discount there.",
|
|
165
|
+
"Local models cost $0 in API fees. n/a = the model has no image input (no visual check) or no known price.",
|
|
166
|
+
"Prices change: set [ai.profiles.\"<model>\"] price_in / price_out in ecadlib.toml to use your own."]:
|
|
167
|
+
n.append([line])
|
|
168
|
+
n.column_dimensions["A"].width = 120
|
|
169
|
+
wb.save(path)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
DOC = """# What it costs to run
|
|
173
|
+
|
|
174
|
+
ecadlib is free and open source. The AI features use **your own API key** (or a model on your own computer),
|
|
175
|
+
so you pay your model provider for what you use, and nothing to this project.
|
|
176
|
+
|
|
177
|
+
Most parts never touch a model. KiCad library parts, EasyEDA parts and standard packages (resistors, capacitors,
|
|
178
|
+
SOIC, QFN and the like) are found or built for free, and so are lifecycle, stock and KiCad upstream checks. A model
|
|
179
|
+
is used only to **read a datasheet** for a part nothing else covers, and for the optional **visual check** that
|
|
180
|
+
compares our footprint and symbol with the datasheet drawing.
|
|
181
|
+
|
|
182
|
+
## Estimated cost per job
|
|
183
|
+
|
|
184
|
+
{table}
|
|
185
|
+
|
|
186
|
+
These are **estimates**, made from the request sizes the code builds and list prices checked on 2026-10-16. They
|
|
187
|
+
are not measured bills.
|
|
188
|
+
|
|
189
|
+
- **Datasheet read:** about {rin:,} tokens in ({pages} picked datasheet pages, the prompt and the output format)
|
|
190
|
+
and about {rout:,} tokens out. On Claude a synchronous read bills its input as a prompt-cache write (1.25x).
|
|
191
|
+
- **Read as batch:** Claude's Message Batches API is half price. ecadlib sends a whole BOM as one batch
|
|
192
|
+
(`ecadlib datasheet batch BOM`). Other providers are called one request at a time.
|
|
193
|
+
- **Visual check:** about {cin:,} tokens in ({imgs} images) and {cout:,} out. It needs a model that reads images
|
|
194
|
+
(n/a otherwise). Answers are cached, so re-running it costs nothing.
|
|
195
|
+
- **50-line BOM:** {nb} of the 50 lines need a datasheet ({share:.0%}), the rest cost $0.
|
|
196
|
+
**500-part library:** {nl} datasheet reads. "Visual check on all 500" is the extra cost of checking every part.
|
|
197
|
+
- **Retries cost more.** An incomplete answer is re-sent with more pages, and with `route = "ladder"` it moves
|
|
198
|
+
to the next model. Budget up to about 2x for hard parts.
|
|
199
|
+
- **Smaller models are cheaper but weaker.** ecadlib adds guide rails for them (see `ecadlib ai rails`), and
|
|
200
|
+
their answers are held for review more often.
|
|
201
|
+
- **Local models** (Ollama, LM Studio, vLLM, llama.cpp) cost nothing in API fees.
|
|
202
|
+
|
|
203
|
+
## Your model, your BOM
|
|
204
|
+
|
|
205
|
+
ecadlib costs # these jobs for the model in your ecadlib.toml
|
|
206
|
+
ecadlib costs --model gemini-3.8-flash
|
|
207
|
+
ecadlib datasheet estimate BOM.csv # this BOM: tokens counted, cost and worst case, nothing sent
|
|
208
|
+
|
|
209
|
+
Set `max_cost_usd` under `[datasheet]` and `[agent.proof]` for a hard cap on spending. If a price here is out of
|
|
210
|
+
date, set your own in `ecadlib.toml`:
|
|
211
|
+
|
|
212
|
+
[ai.profiles."gpt-6-luna"]
|
|
213
|
+
price_in = 0.10 # $ per million input tokens
|
|
214
|
+
price_out = 0.50
|
|
215
|
+
|
|
216
|
+
Prices come from Anthropic's model table for Claude and from public price trackers for the others (benchlm.ai,
|
|
217
|
+
promptunit.ai). Gemini 4 Argon goes to $4/$20 on 2026-12-01 and Gemini 3.8 Flash to $1.50/$7.50 on
|
|
218
|
+
2027-01-01. DeepSeek bills half these rates off-peak. The figures in this page come from `ecadlib costs --all --md
|
|
219
|
+
docs/costs.md`.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def doc(rows, share=SHARE):
|
|
224
|
+
from . import visual_check as V
|
|
225
|
+
table, (rin, rout, cin, cout, nb, nl) = markdown(rows, share)
|
|
226
|
+
return DOC.format(table=table, rin=rin, rout=rout, cin=cin, cout=cout, nb=nb, nl=nl, share=share,
|
|
227
|
+
pages=D.SETTINGS["page_budget"], imgs=V.PROOF_DEFAULTS["max_pages"] + 2)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv=None):
|
|
231
|
+
ap = argparse.ArgumentParser(prog="ecadlib costs", description=__doc__.split("\n\n")[0])
|
|
232
|
+
ap.add_argument("--model", help="model id (default: the one your rules use for datasheets)")
|
|
233
|
+
ap.add_argument("--all", action="store_true", help="every model in the sheet")
|
|
234
|
+
ap.add_argument("--share", type=float, default=SHARE, help=f"share of BOM lines that need a datasheet (default {SHARE})")
|
|
235
|
+
ap.add_argument("--md", help="write the cost page (Markdown) here")
|
|
236
|
+
ap.add_argument("--csv", help="write the sheet as CSV here")
|
|
237
|
+
ap.add_argument("--xlsx", help="write the sheet as an Excel workbook here")
|
|
238
|
+
ap.add_argument("--rules", help="rules file (default: ecadlib.toml found from here)")
|
|
239
|
+
ap.add_argument("--json", action="store_true")
|
|
240
|
+
a = ap.parse_args(argv)
|
|
241
|
+
if not 0 <= a.share <= 1:
|
|
242
|
+
ap.error("--share is a fraction from 0 to 1")
|
|
243
|
+
from . import rules as R
|
|
244
|
+
rules = R.load(a.rules)
|
|
245
|
+
if a.all or a.md or a.csv or a.xlsx:
|
|
246
|
+
rows = sheet(rules, a.share)
|
|
247
|
+
else:
|
|
248
|
+
from .llm import endpoint
|
|
249
|
+
ep = endpoint(rules if isinstance(rules, dict) else None, model=a.model)
|
|
250
|
+
prov = "ollama" if ep["local"] else ep["provider"]
|
|
251
|
+
rows = [dict(jobs(ep["model"], prov, rules, a.share), model=ep["model"])]
|
|
252
|
+
if a.md:
|
|
253
|
+
open(a.md, "w", encoding="utf-8").write(doc(rows, a.share))
|
|
254
|
+
if a.csv:
|
|
255
|
+
write_csv(rows, a.csv)
|
|
256
|
+
if a.xlsx:
|
|
257
|
+
write_xlsx(rows, a.xlsx, a.share)
|
|
258
|
+
if a.json:
|
|
259
|
+
print(json.dumps(rows, indent=1))
|
|
260
|
+
elif not (a.md or a.csv or a.xlsx):
|
|
261
|
+
if len(rows) == 1:
|
|
262
|
+
r = rows[0]
|
|
263
|
+
if r["usd_in"] is None:
|
|
264
|
+
print(f"no price known for {r['id']}: set [ai.profiles.\"{r['id']}\"] price_in / price_out in ecadlib.toml")
|
|
265
|
+
return 1
|
|
266
|
+
print(f"{r['id']}: ${r['usd_in']:g} in / ${r['usd_out']:g} out per million tokens (estimates)")
|
|
267
|
+
for c in COLS[5:]:
|
|
268
|
+
print(f" {HEAD[c]:<26} {money(r[c])}")
|
|
269
|
+
else:
|
|
270
|
+
print(markdown(rows, a.share)[0])
|
|
271
|
+
print("Estimates from request sizes, not a bill. `ecadlib datasheet estimate BOM` counts a real BOM.")
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
sys.exit(main())
|