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/review_cli.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""`ecadlib review`: go through the held parts one at a time, in the terminal.
|
|
2
|
+
|
|
3
|
+
ecadlib review [PLAN] [--workspace W] [--part P] [--yes] [--offline] [--lib DIR ...] [--limit N]
|
|
4
|
+
|
|
5
|
+
For each held part: the reason in plain words and its fix (ecadlib.messages), the datasheet link, and - when a
|
|
6
|
+
symbol or footprint is missing or the pins and pads disagree - the best candidates (ecadlib.resolve). Then one
|
|
7
|
+
question:
|
|
8
|
+
|
|
9
|
+
1-N use that candidate (converted, written into the plan, the part re-checked)
|
|
10
|
+
a release anyway (like `ecadlib release-check --allow PART`; recorded with your name)
|
|
11
|
+
d open the datasheet in the browser
|
|
12
|
+
s skip to the next part q stop here
|
|
13
|
+
|
|
14
|
+
Only when stdin is a terminal. With --yes, or piped, nothing is asked and nothing changes: each held part is printed
|
|
15
|
+
with its reason and the commands that fix it (so it also works in a log or a script). Without PLAN, every library
|
|
16
|
+
plan in the workspace with held parts is reviewed (--workspace, else $ECADLIB_WORKSPACE, else ./ecadlib_workspace).
|
|
17
|
+
Exit code: 0 when nothing is held at the end, 2 otherwise."""
|
|
18
|
+
import argparse, json, os, re, sys, webbrowser
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import messages as M, term as TM
|
|
22
|
+
|
|
23
|
+
CHOICES = "1-{n} use a candidate a release anyway d open the datasheet s skip q quit"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def datasheet_url(item):
|
|
27
|
+
"""The part's datasheet link from its parameters (ComponentLinkNURL next to a "Datasheet" description, or a
|
|
28
|
+
Datasheet / Datasheet URL parameter), or None."""
|
|
29
|
+
pars = {p.get("name"): p.get("value") for p in item.get("parameters") or [] if isinstance(p, dict)}
|
|
30
|
+
for k, v in pars.items():
|
|
31
|
+
m = re.fullmatch(r"ComponentLink(\d+)Description", k or "")
|
|
32
|
+
if m and str(v).strip().lower() == "datasheet" and pars.get(f"ComponentLink{m.group(1)}URL"):
|
|
33
|
+
return pars[f"ComponentLink{m.group(1)}URL"]
|
|
34
|
+
for k, v in sorted(pars.items()): # a link without a description that is plainly a PDF
|
|
35
|
+
if re.fullmatch(r"ComponentLink\d+URL", k or "") and str(v).lower().split("?")[0].endswith(".pdf") \
|
|
36
|
+
and not pars.get(k.replace("URL", "Description")):
|
|
37
|
+
return v
|
|
38
|
+
for k in ("Datasheet", "Datasheet URL", "datasheet"):
|
|
39
|
+
if str(pars.get(k) or "").startswith(("http://", "https://")):
|
|
40
|
+
return pars[k]
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def datasheet_page(reasons):
|
|
45
|
+
"""A page number named in the reasons ("page 34"), for #page=N."""
|
|
46
|
+
for r in reasons:
|
|
47
|
+
m = re.search(r"\bpage (\d+)", str(r))
|
|
48
|
+
if m:
|
|
49
|
+
return int(m.group(1))
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_plans(workspace):
|
|
54
|
+
ws = Path(workspace)
|
|
55
|
+
return sorted(ws.glob("librarian/*/plan.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cache_for(plan, given):
|
|
59
|
+
if given:
|
|
60
|
+
return given
|
|
61
|
+
for c in (Path(plan).parent.parent.parent / "http_cache", Path("out/http_cache")):
|
|
62
|
+
if c.is_dir():
|
|
63
|
+
return str(c)
|
|
64
|
+
return "out/http_cache"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def wants_candidates(item, why):
|
|
68
|
+
from . import resolve as R
|
|
69
|
+
return bool(R.missing(item)) or any(e["code"].startswith(("pins.", "files.")) for e in why)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def candidates(item, plan, src, limit, t):
|
|
73
|
+
"""{kind: [candidate]} for the missing half (or both when pins and pads disagree); errors become a note."""
|
|
74
|
+
from . import resolve as R
|
|
75
|
+
kinds = R.missing(item) or list(R.KINDS)
|
|
76
|
+
out = {}
|
|
77
|
+
for k in kinds:
|
|
78
|
+
try:
|
|
79
|
+
with TM.waiting(f"{k} candidates for {item['name']} (KiCad library index)", t.err):
|
|
80
|
+
out[k] = R.candidates(item, k, Path(plan).parent, src, limit)
|
|
81
|
+
except Exception as e: # network down, no cache: say so, keep going
|
|
82
|
+
text, fix = M.friendly_error(e)
|
|
83
|
+
t.note(f"No {k} candidates right now: {text}", fix + " (--offline uses what is cached)")
|
|
84
|
+
out[k] = []
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def show_part(t, item, plan, cands, src=None):
|
|
89
|
+
"""The card for one held part; returns the numbered candidate list [(kind, candidate)]."""
|
|
90
|
+
why = M.explain_all(item.get("hold_reasons") or [])
|
|
91
|
+
desc = item.get("description") or ""
|
|
92
|
+
t.write(f"{t.status('held')} {t.bold(item['name'])}" + (f" {t.dim(desc)}" if desc else ""))
|
|
93
|
+
for e in why:
|
|
94
|
+
t.wrap(e["text"] + (f" {e['note']}" if e.get("note") else ""), 4)
|
|
95
|
+
t.wrap(f"fix: {e['fix']}", 4)
|
|
96
|
+
if t.verbose:
|
|
97
|
+
for e in why:
|
|
98
|
+
t.write(t.dim(f" ({e['code']}: {e['raw']})"))
|
|
99
|
+
url = datasheet_url(item)
|
|
100
|
+
if url:
|
|
101
|
+
t.write(f" datasheet: {url}")
|
|
102
|
+
numbered = []
|
|
103
|
+
for kind, cs in cands.items():
|
|
104
|
+
if not cs:
|
|
105
|
+
t.write(f" No {kind} candidates found." + " Add your KiCad libraries with --lib" + (", or drop --offline." if a_offline(src) else "."))
|
|
106
|
+
continue
|
|
107
|
+
t.write(f" {kind} candidates:")
|
|
108
|
+
rows = []
|
|
109
|
+
for c in cs:
|
|
110
|
+
numbered.append((kind, c))
|
|
111
|
+
unit = "pins" if kind == "symbol" else "pads"
|
|
112
|
+
fit = (t.status("ready", "fits") if c["match"] else t.status("skipped", "not compared") if c["match"] is None
|
|
113
|
+
else t.status("held", "pins and pads differ"))
|
|
114
|
+
rows.append([f"{len(numbered)}", c["id"], f"{c['count'] if c['count'] is not None else '?'} {unit}", fit
|
|
115
|
+
+ (" (current)" if c.get("current") else "")])
|
|
116
|
+
t.table(rows, indent=6, right=(0,))
|
|
117
|
+
return numbered
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def a_offline(src):
|
|
121
|
+
return bool(getattr(getattr(src, "http", None), "offline", False))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def release_anyway(plan, name, user=None):
|
|
125
|
+
"""release-check --allow for one part; returns the item after the re-check."""
|
|
126
|
+
from . import release_check as RC
|
|
127
|
+
from .approvals import cli_plan_lock, identity, post_check, stamp_allow
|
|
128
|
+
from .fsutil import write_text_atomic
|
|
129
|
+
plan = Path(plan)
|
|
130
|
+
with cli_plan_lock(plan):
|
|
131
|
+
items = json.loads(plan.read_text(encoding="utf-8"))
|
|
132
|
+
it = next(i for i in items if i["name"] == name)
|
|
133
|
+
rows = RC.apply([it], plan.parent, [name])
|
|
134
|
+
stamp_allow([it], [name], identity(user, cwd=plan.parent))
|
|
135
|
+
post_check([it], plan.parent, rows)
|
|
136
|
+
write_text_atomic(plan, json.dumps(items, indent=2))
|
|
137
|
+
return it
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def fix_commands(plan, item, numbered):
|
|
141
|
+
cmds = []
|
|
142
|
+
if numbered:
|
|
143
|
+
kind, c = numbered[0]
|
|
144
|
+
cmds.append(f"ecadlib resolve {plan} --part {item['name']} choose {kind} {c['id']}")
|
|
145
|
+
codes = [e["code"] for e in M.explain_all(item.get("hold_reasons") or [])]
|
|
146
|
+
if any(not c.startswith("files.") for c in codes):
|
|
147
|
+
cmds.append(f"ecadlib release-check {plan} --allow {item['name']}")
|
|
148
|
+
return cmds
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def review_plan(plan, a, t, inp, interactive):
|
|
152
|
+
"""Review one plan's held parts. Returns (held at the start, still held, stopped?)."""
|
|
153
|
+
from . import resolve as R
|
|
154
|
+
held = [h["name"] for h in R.held(plan)]
|
|
155
|
+
if a.part:
|
|
156
|
+
held = [n for n in held if n == a.part]
|
|
157
|
+
if not held:
|
|
158
|
+
return 0, 0, False
|
|
159
|
+
t.write(t.bold(f"{plan}: {M.plural(len(held), 'held part')}"))
|
|
160
|
+
src = None
|
|
161
|
+
still, stopped = 0, False
|
|
162
|
+
for k, name in enumerate(held, 1):
|
|
163
|
+
items = json.loads(Path(plan).read_text(encoding="utf-8"))
|
|
164
|
+
item = next((i for i in items if i["name"] == name), None)
|
|
165
|
+
if not item or item.get("status") != "held":
|
|
166
|
+
continue
|
|
167
|
+
t.write("")
|
|
168
|
+
t.write(t.dim(f"[{k}/{len(held)}]"))
|
|
169
|
+
why = M.explain_all(item.get("hold_reasons") or [])
|
|
170
|
+
cands = {}
|
|
171
|
+
if wants_candidates(item, why) and a.limit > 0:
|
|
172
|
+
src = src or R.Sources(_cache_for(plan, a.cache), a.lib, a.offline)
|
|
173
|
+
cands = candidates(item, plan, src, a.limit, t)
|
|
174
|
+
numbered = show_part(t, item, plan, cands, src)
|
|
175
|
+
if not interactive:
|
|
176
|
+
for c in fix_commands(plan, item, numbered):
|
|
177
|
+
t.write(f" to fix: {c}")
|
|
178
|
+
still += 1
|
|
179
|
+
continue
|
|
180
|
+
status = "held"
|
|
181
|
+
while True:
|
|
182
|
+
fixed_only = all(e["code"].startswith("files.") for e in M.explain_all(item.get("hold_reasons") or []))
|
|
183
|
+
ask = CHOICES.format(n=len(numbered)) if numbered else CHOICES.split(" ", 1)[1]
|
|
184
|
+
if fixed_only: # a missing / broken file cannot be released anyway: do not offer it
|
|
185
|
+
ask = ask.replace(" a release anyway", "").replace("a release anyway ", "")
|
|
186
|
+
try:
|
|
187
|
+
ans = inp(f" {ask}\n > ").strip().lower()
|
|
188
|
+
except EOFError:
|
|
189
|
+
ans = "q"
|
|
190
|
+
if ans in ("", "s", "skip", "n"):
|
|
191
|
+
break
|
|
192
|
+
if ans in ("q", "quit", "exit"):
|
|
193
|
+
stopped = True
|
|
194
|
+
break
|
|
195
|
+
if ans in ("d", "datasheet"):
|
|
196
|
+
url = datasheet_url(item)
|
|
197
|
+
if not url:
|
|
198
|
+
t.note("This part has no datasheet link.", "Add one to the BOM (a Datasheet column) and fill the library again.")
|
|
199
|
+
continue
|
|
200
|
+
page = datasheet_page(item.get("hold_reasons") or [])
|
|
201
|
+
full = url + (f"#page={page}" if page and "#" not in url else "")
|
|
202
|
+
opened = False
|
|
203
|
+
try:
|
|
204
|
+
opened = webbrowser.open(full)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
t.write(f" {'opened' if opened else 'open this in your browser:'} {full}")
|
|
208
|
+
continue
|
|
209
|
+
if ans in ("a", "release", "allow"):
|
|
210
|
+
it = release_anyway(plan, name, a.user)
|
|
211
|
+
status = it.get("status")
|
|
212
|
+
if status == "held":
|
|
213
|
+
left = M.explain_all(it.get("hold_reasons") or [])
|
|
214
|
+
t.write(f" {t.status('held')}: {left[0]['text']} {left[0].get('note') or ''}".rstrip())
|
|
215
|
+
t.write(f" fix: {left[0]['fix']}")
|
|
216
|
+
continue
|
|
217
|
+
t.write(f" {t.status(status, M.status_word(status))}: released anyway, recorded in the plan")
|
|
218
|
+
break
|
|
219
|
+
if ans.isdigit() and 1 <= int(ans) <= len(numbered):
|
|
220
|
+
kind, c = numbered[int(ans) - 1]
|
|
221
|
+
from .approvals import cli_plan_lock
|
|
222
|
+
try:
|
|
223
|
+
with cli_plan_lock(plan), TM.waiting(f"{c['id']} (fetch and convert)", t.err):
|
|
224
|
+
r = R.choose(plan, name, kind, c["id"], src=src, pin_map=c.get("pin_map") or None)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
text, fix = M.friendly_error(e)
|
|
227
|
+
t.note(text, fix)
|
|
228
|
+
continue
|
|
229
|
+
status = r["status"]
|
|
230
|
+
t.write(f" {kind} is now {c['id']}: {t.status(status, M.status_word(status))}")
|
|
231
|
+
for e in M.explain_all(r["hold_reasons"]):
|
|
232
|
+
t.write(f" {e['text']} fix: {e['fix']}")
|
|
233
|
+
for n in r["notes"]:
|
|
234
|
+
t.detail(f" note: {M.clean(n)}")
|
|
235
|
+
if status != "held":
|
|
236
|
+
break
|
|
237
|
+
items = json.loads(Path(plan).read_text(encoding="utf-8"))
|
|
238
|
+
item = next(i for i in items if i["name"] == name)
|
|
239
|
+
continue
|
|
240
|
+
t.write(f" Not one of the choices: {ans!r}.")
|
|
241
|
+
if status == "held":
|
|
242
|
+
still += 1
|
|
243
|
+
if stopped:
|
|
244
|
+
still += sum(1 for _ in held[k:])
|
|
245
|
+
break
|
|
246
|
+
return len(held), still, stopped
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main(argv=None, inp=input):
|
|
250
|
+
ap = argparse.ArgumentParser(prog="ecadlib review", description="go through held parts one at a time: why it is "
|
|
251
|
+
"held in plain words, pick a candidate, release anyway, open the datasheet")
|
|
252
|
+
ap.add_argument("plan", nargs="?", help="a library plan (plan.json); default: every plan in --workspace with held parts")
|
|
253
|
+
ap.add_argument("--workspace", default=os.environ.get("ECADLIB_WORKSPACE") or "ecadlib_workspace")
|
|
254
|
+
ap.add_argument("--part", help="only this part")
|
|
255
|
+
ap.add_argument("--yes", action="store_true", help="never ask: print each held part, its reason and the fix commands")
|
|
256
|
+
ap.add_argument("--lib", action="append", default=[], help="local KiCad library dir for candidates (repeatable)")
|
|
257
|
+
ap.add_argument("--cache", help="HTTP cache (default: the workspace's http_cache)")
|
|
258
|
+
ap.add_argument("--offline", action="store_true", help="candidates from the cache only")
|
|
259
|
+
ap.add_argument("--limit", type=int, default=5, help="candidates shown per missing half (0: none)")
|
|
260
|
+
ap.add_argument("--user", help="who releases anyway (default: $ECADLIB_USER, then git config)")
|
|
261
|
+
a = ap.parse_args(argv)
|
|
262
|
+
t = TM.Term()
|
|
263
|
+
plans = [Path(a.plan)] if a.plan else find_plans(a.workspace)
|
|
264
|
+
if not plans:
|
|
265
|
+
t.err.error("review", f"There is no library plan in {a.workspace} yet.",
|
|
266
|
+
"Fill a library from a BOM first: ecadlib librarian BOM.csv -o <workspace>/librarian/board")
|
|
267
|
+
return 2
|
|
268
|
+
tty = inp is not input or (sys.stdin is not None and sys.stdin.isatty()) # tests pass their own inp
|
|
269
|
+
interactive = tty and not a.yes
|
|
270
|
+
total = still = 0
|
|
271
|
+
for plan in plans:
|
|
272
|
+
n, s, stopped = review_plan(plan, a, t, inp, interactive)
|
|
273
|
+
total, still = total + n, still + s
|
|
274
|
+
if stopped:
|
|
275
|
+
break
|
|
276
|
+
if not total:
|
|
277
|
+
t.summary(f"Nothing is held{' for ' + a.part if a.part else ''}: every part is ready or waiting for sign-off.",
|
|
278
|
+
next=[(f"ecadlib status --workspace {a.workspace}" if not a.plan else f"ecadlib release-check {plans[0]}", "")])
|
|
279
|
+
return 0
|
|
280
|
+
done = total - still
|
|
281
|
+
head = (f"{M.plural(total, 'held part')} reviewed: {done} fixed, {still} still held" if interactive
|
|
282
|
+
else f"{M.plural(still, 'part')} held")
|
|
283
|
+
nxt = [] if interactive else [("ecadlib review" + (f" {a.plan}" if a.plan else f" --workspace {a.workspace}"),
|
|
284
|
+
"in a terminal: pick a fix for each part")]
|
|
285
|
+
if done:
|
|
286
|
+
nxt.append((f"ecadlib release-check {plans[0]}", "check the whole library again"))
|
|
287
|
+
t.summary(head, next=nxt)
|
|
288
|
+
return 2 if still else 0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
sys.exit(main())
|