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/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())
|