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/userconfig.py ADDED
@@ -0,0 +1,257 @@
1
+ """Per-user settings and API keys, for people who set ecadlib up from the setup page instead of a terminal.
2
+
3
+ ecadlib keys list which keys are stored (names only) and which are set in the environment
4
+ ecadlib keys set NAME asks for the value (hidden), stores it for this user
5
+ ecadlib keys remove NAME
6
+
7
+ Where: a folder of this user's own, never the workspace or a repository:
8
+ Windows %APPDATA%\\ecadlib\\ macOS ~/Library/Application Support/ecadlib/
9
+ Linux $XDG_CONFIG_HOME/ecadlib/ (~/.config/ecadlib/) ECADLIB_HOME overrides all three (tests use it)
10
+ settings.json the setup page's answers: workspace folder, tool, library folders, set-up done, what was installed
11
+ keys.json stored API keys: on Windows each value is encrypted for this Windows account (DPAPI, the same store
12
+ browsers use), elsewhere the file is readable by this user only (mode 600), like ~/.netrc
13
+
14
+ How keys are used: every ecadlib entry point (the command line, `ecadlib ui`, the MCP server, the desktop app) calls
15
+ apply_env() first, which copies each stored key into the process environment when that variable is NOT already set.
16
+ So an environment variable always wins, and the rest of ecadlib still reads keys from the environment only. A key
17
+ value is never printed, logged, returned by the web UI or written anywhere else.
18
+
19
+ apply_env() also puts KiCad's bin folder on PATH when kicad-cli is not found there but a KiCad install is (the
20
+ Windows installer of KiCad does not add it to PATH)."""
21
+ import argparse, base64, getpass, json, os, re, sys, tempfile
22
+ from pathlib import Path
23
+
24
+ SETTINGS, KEYS = "settings.json", "keys.json"
25
+ NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,63}")
26
+ MAX_KEY = 4096
27
+
28
+
29
+ def config_dir(environ=None):
30
+ environ = os.environ if environ is None else environ
31
+ if environ.get("ECADLIB_HOME"):
32
+ return Path(environ["ECADLIB_HOME"])
33
+ if os.name == "nt":
34
+ return Path(environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") / "ecadlib"
35
+ if sys.platform == "darwin":
36
+ return Path.home() / "Library" / "Application Support" / "ecadlib"
37
+ return Path(environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "ecadlib"
38
+
39
+
40
+ def documents_dir():
41
+ """The user's Documents folder (Windows: the real one, also when it was moved to OneDrive)."""
42
+ if os.name == "nt":
43
+ try:
44
+ import ctypes
45
+ from ctypes import wintypes
46
+ buf = ctypes.create_unicode_buffer(wintypes.MAX_PATH)
47
+ if ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, buf) == 0 and buf.value: # CSIDL_PERSONAL
48
+ return Path(buf.value)
49
+ except Exception:
50
+ pass
51
+ d = Path.home() / "Documents"
52
+ return d if d.is_dir() or os.name == "nt" or sys.platform == "darwin" else Path.home()
53
+
54
+
55
+ def default_workspace():
56
+ return documents_dir() / "ecadlib"
57
+
58
+
59
+ def _write_private(path, text):
60
+ """Write a file only this user can read (atomic replace; mode 600 where the OS has modes)."""
61
+ path.parent.mkdir(parents=True, exist_ok=True)
62
+ fd, tmp = tempfile.mkstemp(dir=path.parent, prefix="." + path.name + ".")
63
+ try:
64
+ if os.name != "nt":
65
+ os.fchmod(fd, 0o600)
66
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
67
+ f.write(text)
68
+ os.replace(tmp, path)
69
+ except BaseException:
70
+ try:
71
+ os.unlink(tmp)
72
+ except OSError:
73
+ pass
74
+ raise
75
+
76
+
77
+ def _read_json(path):
78
+ try:
79
+ v = json.loads(path.read_text(encoding="utf-8"))
80
+ return v if isinstance(v, dict) else {}
81
+ except (OSError, ValueError):
82
+ return {}
83
+
84
+
85
+ # ------------------------------------------------------------------ settings
86
+ def load_settings(environ=None):
87
+ return _read_json(config_dir(environ) / SETTINGS)
88
+
89
+
90
+ def save_settings(changes, environ=None):
91
+ """Merge `changes` into settings.json; returns the new settings."""
92
+ s = load_settings(environ)
93
+ s.update(changes)
94
+ _write_private(config_dir(environ) / SETTINGS, json.dumps(s, indent=2) + "\n")
95
+ return s
96
+
97
+
98
+ # ------------------------------------------------------------------ keys
99
+ def _dpapi(data, protect):
100
+ """CryptProtectData / CryptUnprotectData for this Windows account (no prompt, no entropy)."""
101
+ import ctypes
102
+ from ctypes import wintypes
103
+
104
+ class Blob(ctypes.Structure):
105
+ _fields_ = [("cbData", wintypes.DWORD), ("pbData", ctypes.POINTER(ctypes.c_char))]
106
+ buf = ctypes.create_string_buffer(data, len(data))
107
+ src, dst = Blob(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))), Blob()
108
+ fn = ctypes.windll.crypt32.CryptProtectData if protect else ctypes.windll.crypt32.CryptUnprotectData
109
+ if not fn(ctypes.byref(src), None, None, None, None, 0x1, ctypes.byref(dst)): # CRYPTPROTECT_UI_FORBIDDEN
110
+ raise OSError(f"Windows could not {'encrypt' if protect else 'decrypt'} the key (error {ctypes.GetLastError()})")
111
+ try:
112
+ return ctypes.string_at(dst.pbData, dst.cbData)
113
+ finally:
114
+ ctypes.windll.kernel32.LocalFree(dst.pbData)
115
+
116
+
117
+ def _encode(value):
118
+ raw = value.encode("utf-8")
119
+ if os.name == "nt":
120
+ return "dpapi:" + base64.b64encode(_dpapi(raw, True)).decode("ascii")
121
+ return "plain:" + base64.b64encode(raw).decode("ascii")
122
+
123
+
124
+ def _decode(stored):
125
+ kind, _, b64 = str(stored).partition(":")
126
+ raw = base64.b64decode(b64)
127
+ if kind == "dpapi":
128
+ if os.name != "nt":
129
+ raise OSError("this key was stored on Windows and can only be read there")
130
+ raw = _dpapi(raw, False)
131
+ elif kind != "plain":
132
+ raise ValueError(f"unknown key format {kind!r}")
133
+ return raw.decode("utf-8")
134
+
135
+
136
+ def check_name(name):
137
+ if not isinstance(name, str) or not NAME_RE.fullmatch(name):
138
+ raise ValueError(f"{name!r} is not an environment variable name (letters, digits and _)")
139
+ return name
140
+
141
+
142
+ def stored_names(environ=None):
143
+ return sorted(_read_json(config_dir(environ) / KEYS))
144
+
145
+
146
+ def set_key(name, value, environ=None):
147
+ check_name(name)
148
+ value = (value or "").strip()
149
+ if not value:
150
+ raise ValueError("the key is empty")
151
+ if len(value) > MAX_KEY or any(c in value for c in "\r\n\0"):
152
+ raise ValueError("that does not look like an API key (too long, or more than one line)")
153
+ f = config_dir(environ) / KEYS
154
+ keys = _read_json(f)
155
+ keys[name] = _encode(value)
156
+ _write_private(f, json.dumps(keys, indent=2) + "\n")
157
+
158
+
159
+ def remove_key(name, environ=None):
160
+ f = config_dir(environ) / KEYS
161
+ keys = _read_json(f)
162
+ if keys.pop(check_name(name), None) is None:
163
+ return False
164
+ _write_private(f, json.dumps(keys, indent=2) + "\n")
165
+ return True
166
+
167
+
168
+ def get_key(name, environ=None):
169
+ """The stored value, or None (unreadable entries are skipped). For apply_env and the key check only."""
170
+ stored = _read_json(config_dir(environ) / KEYS).get(name)
171
+ if stored is None:
172
+ return None
173
+ try:
174
+ return _decode(stored)
175
+ except (OSError, ValueError):
176
+ return None
177
+
178
+
179
+ def apply_env(environ=None, config_environ=None):
180
+ """Stored keys -> the environment where the variable is not set; KiCad's bin folder -> PATH when kicad-cli is
181
+ missing. Returns the names it set. Never raises: a broken config folder must not stop any command."""
182
+ environ = os.environ if environ is None else environ
183
+ done = []
184
+ try:
185
+ for name in stored_names(config_environ or environ):
186
+ if NAME_RE.fullmatch(name) and not environ.get(name):
187
+ v = get_key(name, config_environ or environ)
188
+ if v:
189
+ environ[name] = v
190
+ done.append(name)
191
+ except Exception:
192
+ pass
193
+ try:
194
+ import shutil
195
+ if not shutil.which("kicad-cli", path=environ.get("PATH")):
196
+ from . import detect
197
+ cli = detect.kicad_cli()
198
+ if cli:
199
+ environ["PATH"] = environ.get("PATH", "") + os.pathsep + str(Path(cli).parent)
200
+ except Exception:
201
+ pass
202
+ return done
203
+
204
+
205
+ # ------------------------------------------------------------------ how to run ecadlib from another program
206
+ def self_command():
207
+ """The command line that runs this ecadlib: the bundled ecadlib.exe in the Windows app, else this Python."""
208
+ if getattr(sys, "frozen", False):
209
+ exe = Path(sys.executable)
210
+ cli = exe.with_name("ecadlib.exe" if os.name == "nt" else "ecadlib")
211
+ return [str(cli if cli.is_file() else exe)]
212
+ return [sys.executable, "-m", "ecadlib"]
213
+
214
+
215
+ def key_hint(name):
216
+ """How to set a key, the same on every OS."""
217
+ return f"add it on the setup page (`ecadlib setup`), or run: ecadlib keys set {name}"
218
+
219
+
220
+ # ------------------------------------------------------------------ ecadlib keys
221
+ def main(argv=None, inp=None, out=print):
222
+ ap = argparse.ArgumentParser(prog="ecadlib keys", description="API keys stored for this user (names only are shown)")
223
+ sub = ap.add_subparsers(dest="cmd", required=True)
224
+ sub.add_parser("list", help="stored keys and which are set in the environment")
225
+ s = sub.add_parser("set", help="store a key (asked for, hidden); an environment variable of the same name still wins")
226
+ s.add_argument("name", help="the variable name, e.g. ANTHROPIC_API_KEY")
227
+ s.add_argument("--stdin", action="store_true", help="read the value from standard input (scripts)")
228
+ r = sub.add_parser("remove", help="forget a stored key")
229
+ r.add_argument("name")
230
+ a = ap.parse_args(argv)
231
+ if a.cmd == "list":
232
+ names = stored_names()
233
+ out(f"stored in {config_dir() / KEYS}" + (" (encrypted for this Windows account)" if os.name == "nt" else ""))
234
+ if not names:
235
+ out(" none stored")
236
+ for n in names:
237
+ env = "also set in the environment, which wins" if os.environ.get(n) and os.environ.get(n) != get_key(n) else ""
238
+ out(f" {n}" + (f" ({env})" if env else ""))
239
+ return 0
240
+ if a.cmd == "set":
241
+ check_name(a.name)
242
+ if a.stdin:
243
+ value = sys.stdin.readline()
244
+ else:
245
+ value = (inp or getpass.getpass)(f"{a.name} (input hidden): ")
246
+ set_key(a.name, value)
247
+ out(f"stored {a.name} for this user; every ecadlib command uses it from now on")
248
+ return 0
249
+ if remove_key(a.name):
250
+ out(f"removed {a.name}")
251
+ return 0
252
+ out(f"{a.name} was not stored")
253
+ return 1
254
+
255
+
256
+ if __name__ == "__main__":
257
+ sys.exit(main())