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/reports.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""The reports people open, in the shared voice (docs/ui-voice.md): outcome first, then what the reader must do,
|
|
2
|
+
grouped by action, in plain words. One document structure renders as Markdown and as a self-contained HTML page with
|
|
3
|
+
the web UI's look (light and dark), so both always say the same.
|
|
4
|
+
|
|
5
|
+
part_view(item) -> one plan item as people see it: status word, reasons + fixes, sources
|
|
6
|
+
review_doc(items, bom) -> the library review (librarian review.md / review.html)
|
|
7
|
+
to_md(doc) / to_html(doc) -> text
|
|
8
|
+
write_review_files(items, out, bom, details_md) -> writes <out>/review.md and <out>/review.html
|
|
9
|
+
|
|
10
|
+
A doc is {"title", "lead": [lines], "next": [lines], "sections": [{"title", "intro", "parts": [part], "table":
|
|
11
|
+
(header, rows)}], "details": [markdown lines]}; a part is {"name", "status", "word", "lines": [{"text", "fix",
|
|
12
|
+
"note", "code"}], "meta": [(label, value)]}."""
|
|
13
|
+
import html, re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from . import messages as M
|
|
17
|
+
|
|
18
|
+
CHIP = {"ready": "ok", "held": "bad", "awaiting_approval": "info", "duplicate": "off", "existing": "off"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tried_line(t):
|
|
22
|
+
"""A librarian "what was tried" entry with its source in words: "standard: footprint ..." -> "standard parts: ..."."""
|
|
23
|
+
t = M.clean(t)
|
|
24
|
+
m = re.match(r"(kicad|standard|easyeda|local|datasheet)( reader)?\b", t)
|
|
25
|
+
return M.SOURCES[m.group(1)] + t[m.end():] if m else t
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _par(it, *names):
|
|
29
|
+
p = {x.get("name"): x.get("value") for x in it.get("parameters") or []}
|
|
30
|
+
return next((str(p[n]) for n in names if str(p.get(n) or "").strip()), "")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def part_view(it):
|
|
34
|
+
"""A plan item -> {name, status, word, reasons, checks, meta, tried, datasheet}. reasons: why it is held (or waits);
|
|
35
|
+
checks: what a person should look at even when it is ready. Each is an explain() dict (code kept for JSON)."""
|
|
36
|
+
st = it.get("status") or ""
|
|
37
|
+
rc = it.get("release_check") or {}
|
|
38
|
+
lib = it.get("librarian") or {}
|
|
39
|
+
reasons = M.explain_all(it.get("hold_reasons") or []) if st == "held" else []
|
|
40
|
+
if st == "awaiting_approval":
|
|
41
|
+
miss = "; ".join((it.get("approval") or {}).get("missing") or [])
|
|
42
|
+
reasons.append(M.explain("awaiting approval" + (f": {miss}" if miss else "")))
|
|
43
|
+
if st == "duplicate" or it.get("duplicate_of"):
|
|
44
|
+
reasons.append(M.explain(f"duplicate of {it.get('duplicate_of') or '?'}"))
|
|
45
|
+
if st == "deleted_in_workspace":
|
|
46
|
+
reasons.append(M.explain("deleted in the Altium 365 workspace"))
|
|
47
|
+
have = {(r["text"], r["fix"]) for r in reasons}
|
|
48
|
+
checks = [c for c in M.explain_all(lib.get("review") or []) if (c["text"], c["fix"]) not in have]
|
|
49
|
+
if st == "held": # a disagreeing cross-check is why nothing was written: say it first
|
|
50
|
+
first = [c for c in checks if c["code"] == "crosscheck.disagree"]
|
|
51
|
+
reasons, checks = first + reasons, [c for c in checks if c not in first]
|
|
52
|
+
cc = (it.get("cross_check") or {}).get("status")
|
|
53
|
+
if cc == "unchecked":
|
|
54
|
+
checks.append(M.explain("cross-check unchecked: " + str((it.get("cross_check") or {}).get("summary") or "")))
|
|
55
|
+
word = M.status_word("allowed" if rc.get("result") == "allowed" else st)
|
|
56
|
+
src = it.get("source") or {}
|
|
57
|
+
meta = [("Symbol from", M.source_word(src.get("symbol"))), ("Footprint from", M.source_word(src.get("footprint")))]
|
|
58
|
+
if cc:
|
|
59
|
+
meta.append(("Cross-check", {"agreed": "agreed with a second source", "disagreed": "disagreed (held)",
|
|
60
|
+
"unchecked": "could not be made", "skipped": "not needed"}.get(cc, cc)))
|
|
61
|
+
if rc.get("parity"):
|
|
62
|
+
meta.append(("KiCad and Altium", "match" if rc["parity"] == "match" else "differ"))
|
|
63
|
+
if rc.get("height"):
|
|
64
|
+
meta.append(("Height", f"{rc['height']:.2f} mm"))
|
|
65
|
+
if it.get("allow"):
|
|
66
|
+
a = it["allow"]
|
|
67
|
+
meta.append(("Released anyway", " ".join(x for x in (str(a.get("by") or ""), str(a.get("date") or "")) if x)))
|
|
68
|
+
ds = _par(it, "ComponentLink2URL", "Datasheet")
|
|
69
|
+
return {"name": it.get("name", "?"), "status": st, "word": word, "reasons": reasons, "checks": checks, "meta": meta,
|
|
70
|
+
"tried": [tried_line(t) for t in lib.get("tried") or []], "datasheet": ds if ds.startswith("http") else ""}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _lines(view, with_checks=True):
|
|
74
|
+
out = [dict(r) for r in view["reasons"]]
|
|
75
|
+
if with_checks:
|
|
76
|
+
out += [dict(c, also=bool(view["reasons"])) for c in view["checks"]]
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def review_doc(items, bom=""):
|
|
81
|
+
views = [part_view(it) for it in items]
|
|
82
|
+
by = lambda *s: [v for v in views if v["status"] in s]
|
|
83
|
+
held, waiting, dup = by("held"), by("awaiting_approval"), by("duplicate")
|
|
84
|
+
pulled = by("existing", "deleted_in_workspace")
|
|
85
|
+
ready = by("ready")
|
|
86
|
+
check = [v for v in ready if v["checks"]]
|
|
87
|
+
name = Path(str(bom)).name if bom else ""
|
|
88
|
+
lead = [f"**{M.plural(len(views), 'part')}: {M.counts(v['status'] for v in views)}.**"]
|
|
89
|
+
todo = len(held) + len(waiting) + len(check)
|
|
90
|
+
if todo:
|
|
91
|
+
lead.append(" ".join(x for x in (
|
|
92
|
+
f"{M.plural(len(held), 'part')} {'is' if len(held) == 1 else 'are'} held: fix {'it' if len(held) == 1 else 'them'} first." if held else "",
|
|
93
|
+
f"{len(waiting)} waiting for sign-off." if waiting else "",
|
|
94
|
+
f"{M.plural(len(check), 'ready part')} to double-check." if check else "") if x))
|
|
95
|
+
else:
|
|
96
|
+
lead.append("Nothing needs a person.")
|
|
97
|
+
nxt = []
|
|
98
|
+
if held:
|
|
99
|
+
nxt.append("Go through the held parts one at a time with `ecadlib review` in a terminal, or in `ecadlib ui` (Fill the library): **Pick…** a symbol or footprint, or **Release anyway** after you have looked.")
|
|
100
|
+
if waiting:
|
|
101
|
+
nxt.append("Ask a reviewer to approve the parts waiting for sign-off (`ecadlib ui`, Sign-off).")
|
|
102
|
+
if not held and not waiting:
|
|
103
|
+
nxt.append("Send the library to Altium 365 or KiCad (`ecadlib ui`, step 3), or run `ecadlib status` for the whole workspace.")
|
|
104
|
+
part = lambda v, wc=True: {"name": v["name"], "status": v["status"], "word": v["word"], "lines": _lines(v, wc),
|
|
105
|
+
"meta": v["meta"], "tried": v["tried"], "datasheet": v["datasheet"]}
|
|
106
|
+
sections = []
|
|
107
|
+
if held:
|
|
108
|
+
sections.append({"id": "held", "title": f"Held: fix these first ({len(held)})",
|
|
109
|
+
"intro": "Each held part says what is wrong and what to do. It stays out of the library until it is fixed or released anyway.",
|
|
110
|
+
"parts": [part(v) for v in held]})
|
|
111
|
+
if waiting:
|
|
112
|
+
sections.append({"id": "waiting", "title": f"Waiting for sign-off ({len(waiting)})", "parts": [part(v) for v in waiting]})
|
|
113
|
+
if check:
|
|
114
|
+
sections.append({"id": "check", "title": f"Ready, but check before you release ({len(check)})",
|
|
115
|
+
"intro": "These passed every check; a person should still look at the points below.",
|
|
116
|
+
"parts": [part(v) for v in check]})
|
|
117
|
+
if dup:
|
|
118
|
+
sections.append({"id": "duplicate", "title": f"Duplicates ({len(dup)})", "parts": [part(v) for v in dup]})
|
|
119
|
+
if ready:
|
|
120
|
+
rows = [[v["name"], v["word"]] + [dict(v["meta"]).get(k, "") for k in ("Symbol from", "Footprint from", "Cross-check", "KiCad and Altium")]
|
|
121
|
+
for v in ready]
|
|
122
|
+
sections.append({"id": "ready", "title": f"Ready ({len(ready)})",
|
|
123
|
+
"table": (["Part", "Status", "Symbol from", "Footprint from", "Cross-check", "KiCad and Altium"], rows)})
|
|
124
|
+
if pulled:
|
|
125
|
+
sections.append({"id": "a365", "title": f"Already in Altium 365 ({len(pulled)})",
|
|
126
|
+
"table": (["Part", "Status"], [[v["name"], v["word"]] for v in pulled])})
|
|
127
|
+
return {"title": f"Library review{': ' + name if name else ''}", "lead": lead, "next": nxt, "sections": sections,
|
|
128
|
+
"details": []}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------- Markdown
|
|
132
|
+
def _cell(s):
|
|
133
|
+
return str(s if s not in (None, "") else "-").replace("|", "/").replace("\n", " ")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _table_md(header, rows):
|
|
137
|
+
return ["| " + " | ".join(header) + " |", "|" + "---|" * len(header)] + ["| " + " | ".join(_cell(c) for c in r) + " |" for r in rows]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def to_md(doc):
|
|
141
|
+
md = [f"# {doc['title']}", ""] + [x for line in doc["lead"] for x in (line, "")]
|
|
142
|
+
if doc.get("next"):
|
|
143
|
+
md += ["**Next:**", ""] + [f"{i}. {n}" for i, n in enumerate(doc["next"], 1)] + [""]
|
|
144
|
+
for s in doc["sections"]:
|
|
145
|
+
md += [f"## {s['title']}", ""]
|
|
146
|
+
if s.get("intro"):
|
|
147
|
+
md += [s["intro"], ""]
|
|
148
|
+
for p in s.get("parts") or []:
|
|
149
|
+
md += [f"### {p['name']} ({p['word']})", ""]
|
|
150
|
+
for ln in p["lines"]:
|
|
151
|
+
md.append(f"- {'Also check: ' if ln.get('also') else ''}{ln['text']}"
|
|
152
|
+
+ (f" {ln['note']}" if ln.get("note") else "") + f" \n **Next:** {ln['fix']}")
|
|
153
|
+
if p["lines"]:
|
|
154
|
+
md.append("")
|
|
155
|
+
meta = [f"{k}: {v}" for k, v in p["meta"]] + ([f"Datasheet: <{p['datasheet']}>"] if p.get("datasheet") else [])
|
|
156
|
+
md += [" · ".join(meta), ""]
|
|
157
|
+
if p["status"] == "held" and p.get("tried") and any(ln.get("code", "").startswith("files.") for ln in p["lines"]):
|
|
158
|
+
md += ["What was tried: " + "; ".join(p["tried"]), ""]
|
|
159
|
+
if s.get("table"):
|
|
160
|
+
md += _table_md(*s["table"]) + [""]
|
|
161
|
+
if doc.get("details"):
|
|
162
|
+
md += ["## Details", ""] + list(doc["details"])
|
|
163
|
+
return "\n".join(md).rstrip() + "\n"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------- HTML (the web UI's tokens and chips)
|
|
167
|
+
CSS = """:root{--bg:#f6f7f9;--panel:#fff;--text:#1b1f24;--muted:#5d6673;--line:#dde1e6;--accent:#2357d9;
|
|
168
|
+
--ok:#1d7a46;--ok-bg:#e3f4ea;--warn:#9a5b00;--warn-bg:#fdf1dc;--bad:#b3261e;--bad-bg:#fbe6e4;--info:#3d4aa8;
|
|
169
|
+
--info-bg:#e9ebfb;--off:#5d6673;--off-bg:#eceef1}
|
|
170
|
+
@media (prefers-color-scheme:dark){:root{--bg:#14171b;--panel:#1c2026;--text:#e6e9ed;--muted:#9aa3ad;--line:#2e343c;
|
|
171
|
+
--accent:#7ea2ff;--ok:#6fd39a;--ok-bg:#173325;--warn:#f0b660;--warn-bg:#3a2c12;--bad:#ff8a80;--bad-bg:#3d1d1b;
|
|
172
|
+
--info:#a9b3ff;--info-bg:#262a4a;--off:#9aa3ad;--off-bg:#262b32}}
|
|
173
|
+
*{box-sizing:border-box}body{margin:0;font:14px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--bg);color:var(--text)}
|
|
174
|
+
header{padding:14px 20px;border-bottom:1px solid var(--line);background:var(--panel)}header h1{font-size:18px;margin:0}
|
|
175
|
+
main{max-width:1100px;margin:0 auto;padding:18px 16px 60px}a{color:var(--accent)}
|
|
176
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
|
177
|
+
.card h2{font-size:15px;margin:0 0 6px}.lead{font-size:15px}.muted{color:var(--muted)}
|
|
178
|
+
.part{border-top:1px solid var(--line);padding:12px 0}.part:first-of-type{border-top:0}
|
|
179
|
+
.part h3{font-size:14px;margin:0 0 6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
180
|
+
.chip{display:inline-block;border-radius:999px;padding:1px 9px;font-size:12px;font-weight:600}
|
|
181
|
+
.chip.ok{background:var(--ok-bg);color:var(--ok)}.chip.bad{background:var(--bad-bg);color:var(--bad)}
|
|
182
|
+
.chip.info{background:var(--info-bg);color:var(--info)}.chip.off{background:var(--off-bg);color:var(--off)}
|
|
183
|
+
.chip.warn{background:var(--warn-bg);color:var(--warn)}
|
|
184
|
+
ul.why{margin:4px 0;padding-left:18px}ul.why li{margin:4px 0}.fix{display:block;color:var(--muted)}
|
|
185
|
+
.fix b{color:var(--text)}.meta{color:var(--muted);font-size:12.5px;overflow-wrap:anywhere}
|
|
186
|
+
.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}
|
|
187
|
+
th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top}th{color:var(--muted);font-weight:600}
|
|
188
|
+
details summary{cursor:pointer;color:var(--muted)}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
|
|
189
|
+
@media (max-width:600px){main{padding:12px 10px}th,td{padding:5px 4px}}"""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _inline(s):
|
|
193
|
+
t = html.escape(str(s))
|
|
194
|
+
t = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", t)
|
|
195
|
+
t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t)
|
|
196
|
+
t = re.sub(r"<(https?://[^&\s]+)>", lambda m: f'<a href="{m.group(1)}">{m.group(1)}</a>', t)
|
|
197
|
+
return t
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def md_to_html(lines):
|
|
201
|
+
"""The small Markdown the reports use (headings, bullets, numbered lists, tables, paragraphs) -> HTML."""
|
|
202
|
+
out, i, lines = [], 0, list(lines)
|
|
203
|
+
while i < len(lines):
|
|
204
|
+
ln = lines[i]
|
|
205
|
+
if not ln.strip():
|
|
206
|
+
i += 1
|
|
207
|
+
elif ln.startswith("#"):
|
|
208
|
+
lvl = min(len(ln) - len(ln.lstrip("#")) + 1, 4)
|
|
209
|
+
out.append(f"<h{lvl}>{_inline(ln.lstrip('#').strip())}</h{lvl}>"); i += 1
|
|
210
|
+
elif ln.startswith("|"):
|
|
211
|
+
rows = []
|
|
212
|
+
while i < len(lines) and lines[i].startswith("|"):
|
|
213
|
+
if not re.fullmatch(r"\|(\s*:?-+:?\s*\|)+", lines[i].strip()):
|
|
214
|
+
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")])
|
|
215
|
+
i += 1
|
|
216
|
+
head, body = rows[0], rows[1:]
|
|
217
|
+
out.append('<div class="scroll"><table><thead><tr>' + "".join(f"<th>{_inline(c)}</th>" for c in head) + "</tr></thead><tbody>"
|
|
218
|
+
+ "".join("<tr>" + "".join(f"<td>{_inline(c)}</td>" for c in r) + "</tr>" for r in body) + "</tbody></table></div>")
|
|
219
|
+
elif re.match(r"\s*([-*]|\d+\.) ", ln):
|
|
220
|
+
tag = "ol" if re.match(r"\s*\d+\. ", ln) else "ul"
|
|
221
|
+
items = []
|
|
222
|
+
while i < len(lines) and re.match(r"\s*([-*]|\d+\.) ", lines[i]):
|
|
223
|
+
items.append(re.sub(r"^\s*([-*]|\d+\.) ", "", lines[i])); i += 1
|
|
224
|
+
out.append(f"<{tag}>" + "".join(f"<li>{_inline(x)}</li>" for x in items) + f"</{tag}>")
|
|
225
|
+
else:
|
|
226
|
+
para = []
|
|
227
|
+
while i < len(lines) and lines[i].strip() and not re.match(r"(#|\||\s*([-*]|\d+\.) )", lines[i]):
|
|
228
|
+
para.append(lines[i]); i += 1
|
|
229
|
+
out.append(f"<p>{'<br>'.join(_inline(p) for p in para)}</p>")
|
|
230
|
+
return "\n".join(out)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def to_html(doc):
|
|
234
|
+
e = html.escape
|
|
235
|
+
body = ['<div class="card">'] + [f'<p class="lead">{_inline(x)}</p>' for x in doc["lead"]]
|
|
236
|
+
if doc.get("next"):
|
|
237
|
+
body.append("<p><b>Next:</b></p><ol>" + "".join(f"<li>{_inline(n)}</li>" for n in doc["next"]) + "</ol>")
|
|
238
|
+
body.append("</div>")
|
|
239
|
+
for s in doc["sections"]:
|
|
240
|
+
body.append(f'<section class="card" id="{e(s.get("id", ""))}"><h2>{e(s["title"])}</h2>')
|
|
241
|
+
if s.get("intro"):
|
|
242
|
+
body.append(f'<p class="muted">{e(s["intro"])}</p>')
|
|
243
|
+
for p in s.get("parts") or []:
|
|
244
|
+
body.append(f'<div class="part"><h3>{e(p["name"])} <span class="chip {CHIP.get(p["status"], "warn")}">{e(p["word"])}</span></h3>')
|
|
245
|
+
if p["lines"]:
|
|
246
|
+
body.append('<ul class="why">' + "".join(
|
|
247
|
+
f"<li>{'Also check: ' if ln.get('also') else ''}{e(ln['text'])}{(' ' + e(ln['note'])) if ln.get('note') else ''}"
|
|
248
|
+
f'<span class="fix"><b>Next:</b> {e(ln["fix"])}</span></li>' for ln in p["lines"]) + "</ul>")
|
|
249
|
+
meta = [f"{e(k)}: {e(v)}" for k, v in p["meta"]]
|
|
250
|
+
if p.get("datasheet"):
|
|
251
|
+
meta.append(f'<a href="{e(p["datasheet"])}">Datasheet</a>')
|
|
252
|
+
body.append(f'<div class="meta">{" · ".join(meta)}</div>')
|
|
253
|
+
if p["status"] == "held" and p.get("tried") and any(ln.get("code", "").startswith("files.") for ln in p["lines"]):
|
|
254
|
+
body.append(f'<div class="meta">What was tried: {e("; ".join(p["tried"]))}</div>')
|
|
255
|
+
body.append("</div>")
|
|
256
|
+
if s.get("table"):
|
|
257
|
+
body.append(md_to_html(_table_md(*s["table"])))
|
|
258
|
+
body.append("</section>")
|
|
259
|
+
if doc.get("details"):
|
|
260
|
+
body.append(f'<section class="card" id="details"><details><summary>Details</summary>{md_to_html(doc["details"])}</details></section>')
|
|
261
|
+
return ("<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">"
|
|
262
|
+
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
|
263
|
+
f"<title>{e(doc['title'])}</title><style>{CSS}</style></head>\n<body><header><h1>{e(doc['title'])}</h1></header>"
|
|
264
|
+
f"<main>\n" + "\n".join(body) + "\n</main></body></html>\n")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def write_review_files(items, out, bom="", details=()):
|
|
268
|
+
"""<out>/review.md and <out>/review.html from the plan items; details: Markdown lines for the Details section."""
|
|
269
|
+
doc = review_doc(items, bom)
|
|
270
|
+
doc["details"] = [M.clean(x) for x in details]
|
|
271
|
+
out = Path(out)
|
|
272
|
+
(out / "review.md").write_text(to_md(doc), encoding="utf-8")
|
|
273
|
+
(out / "review.html").write_text(to_html(doc), encoding="utf-8")
|
|
274
|
+
return doc
|