ecadlib 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/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())