added automation, updated tables and read.me
This commit is contained in:
213
update_component_index.py
Normal file
213
update_component_index.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parses all .kicad_sym files in symbols/ and updates the Component Index
|
||||
table in README.md with the actual contents of the library.
|
||||
|
||||
Reads the following fields from each symbol:
|
||||
Value, MPN, Digikey_PN, Manufacturer, Footprint, ki_description (or Description)
|
||||
|
||||
Run locally: python3 .github/scripts/update_component_index.py
|
||||
Run in CI: triggered automatically on push to symbols/
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
|
||||
SYMBOLS_DIR = Path("symbols")
|
||||
README_PATH = Path("README.md")
|
||||
|
||||
# Maps symbol filename (without extension) to a human-readable category label
|
||||
LIBRARY_LABELS = {
|
||||
"0_ic_logic": "0_ic_logic",
|
||||
"0_ic_mcu": "0_ic_mcu",
|
||||
"0_ic_driver": "0_ic_driver",
|
||||
"0_ic_power": "0_ic_power",
|
||||
"0_ic_analog": "0_ic_analog",
|
||||
"0_ic_rf": "0_ic_rf",
|
||||
"0_ic_interface": "0_ic_interface",
|
||||
"0_passive": "0_passive",
|
||||
"0_connector": "0_connector",
|
||||
"0_discrete": "0_discrete",
|
||||
}
|
||||
|
||||
# Order categories should appear in the table
|
||||
CATEGORY_ORDER = [
|
||||
"0_ic_logic",
|
||||
"0_ic_driver",
|
||||
"0_ic_power",
|
||||
"0_ic_analog",
|
||||
"0_ic_mcu",
|
||||
"0_ic_rf",
|
||||
"0_ic_interface",
|
||||
"0_passive",
|
||||
"0_connector",
|
||||
"0_discrete",
|
||||
]
|
||||
|
||||
|
||||
def parse_field(symbol_block: str, field_name: str) -> str:
|
||||
"""Extract a named field value from a symbol block."""
|
||||
# KiCad sym format: (property "FieldName" "Value" ...)
|
||||
pattern = rf'\(property\s+"{re.escape(field_name)}"\s+"([^"]*)"'
|
||||
match = re.search(pattern, symbol_block)
|
||||
return match.group(1).strip() if match else ""
|
||||
|
||||
|
||||
def parse_symbols_from_file(filepath: Path) -> list[dict]:
|
||||
"""
|
||||
Parse all symbols from a .kicad_sym file.
|
||||
Returns a list of dicts with keys: name, mpn, description, footprint, digikey_pn, manufacturer
|
||||
"""
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
symbols = []
|
||||
|
||||
# Find all top-level symbol blocks
|
||||
# Each starts with: (symbol "NAME"
|
||||
symbol_pattern = re.compile(r'\(symbol\s+"([^"]+)"(?!\s+\(extends)', re.MULTILINE)
|
||||
|
||||
matches = list(symbol_pattern.finditer(content))
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
name = match.group(1)
|
||||
|
||||
# Skip sub-units (contain _ followed by digits at the end e.g. "SN74HC193_0_1")
|
||||
# KiCad uses these internally for multi-unit symbols
|
||||
if re.search(r'_\d+_\d+$', name):
|
||||
continue
|
||||
|
||||
# Extract the block for this symbol (up to next top-level symbol or end)
|
||||
start = match.start()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
||||
block = content[start:end]
|
||||
|
||||
mpn = parse_field(block, "MPN") or parse_field(block, "Value") or name
|
||||
description = (parse_field(block, "ki_description")
|
||||
or parse_field(block, "Description")
|
||||
or parse_field(block, "description")
|
||||
or "")
|
||||
footprint = parse_field(block, "Footprint")
|
||||
digikey_pn = parse_field(block, "Digikey_PN") or parse_field(block, "Digikey PN") or "-"
|
||||
manufacturer = parse_field(block, "Manufacturer") or "-"
|
||||
|
||||
# Shorten footprint for table readability — keep library:name but trim long paths
|
||||
if footprint and ":" in footprint:
|
||||
lib_part, fp_name = footprint.split(":", 1)
|
||||
# Truncate very long footprint names
|
||||
if len(fp_name) > 30:
|
||||
fp_name = fp_name[:27] + "..."
|
||||
footprint = f"{lib_part}:{fp_name}"
|
||||
|
||||
symbols.append({
|
||||
"name": name,
|
||||
"mpn": mpn,
|
||||
"description": description,
|
||||
"footprint": footprint or "-",
|
||||
"digikey_pn": digikey_pn,
|
||||
"manufacturer": manufacturer,
|
||||
})
|
||||
|
||||
return symbols
|
||||
|
||||
|
||||
def build_component_table(all_symbols: dict[str, list[dict]]) -> str:
|
||||
"""Build the full markdown component index table."""
|
||||
lines = []
|
||||
lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |")
|
||||
lines.append("|---|---|---|---|---|---|")
|
||||
|
||||
total = 0
|
||||
for category in CATEGORY_ORDER:
|
||||
symbols = all_symbols.get(category, [])
|
||||
if not symbols:
|
||||
continue
|
||||
for s in sorted(symbols, key=lambda x: x["mpn"]):
|
||||
lines.append(
|
||||
f"| {s['mpn']} "
|
||||
f"| {s['description'] or '—'} "
|
||||
f"| {s['manufacturer']} "
|
||||
f"| {category} "
|
||||
f"| {s['footprint']} "
|
||||
f"| {s['digikey_pn']} |"
|
||||
)
|
||||
total += 1
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"*{total} components — auto-generated {date.today().isoformat()}*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def update_readme(new_table: str) -> bool:
|
||||
"""
|
||||
Replace the content between the Component Index header and the next
|
||||
top-level ## header in README.md with the new table.
|
||||
Returns True if the file was changed.
|
||||
"""
|
||||
readme = README_PATH.read_text(encoding="utf-8")
|
||||
|
||||
# Match from ## Component Index to the next ## section or end of file
|
||||
pattern = re.compile(
|
||||
r'(## Component Index\n+)(.*?)(\n## |\Z)',
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
match = pattern.search(readme)
|
||||
if not match:
|
||||
print("ERROR: Could not find '## Component Index' section in README.md")
|
||||
return False
|
||||
|
||||
original_block = match.group(2)
|
||||
new_block = new_table + "\n\n"
|
||||
|
||||
if original_block == new_block:
|
||||
return False
|
||||
|
||||
updated_readme = (
|
||||
readme[:match.start(2)]
|
||||
+ new_block
|
||||
+ readme[match.start(3):]
|
||||
)
|
||||
|
||||
README_PATH.write_text(updated_readme, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if not SYMBOLS_DIR.exists():
|
||||
print(f"ERROR: symbols/ directory not found. Run from repo root.")
|
||||
return
|
||||
|
||||
if not README_PATH.exists():
|
||||
print(f"ERROR: README.md not found. Run from repo root.")
|
||||
return
|
||||
|
||||
all_symbols: dict[str, list[dict]] = {}
|
||||
|
||||
for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")):
|
||||
lib_name = sym_file.stem
|
||||
symbols = parse_symbols_from_file(sym_file)
|
||||
if symbols:
|
||||
all_symbols[lib_name] = symbols
|
||||
print(f" {lib_name}: {len(symbols)} symbol(s)")
|
||||
else:
|
||||
print(f" {lib_name}: empty")
|
||||
|
||||
if not any(all_symbols.values()):
|
||||
print("No symbols found — README not updated.")
|
||||
return
|
||||
|
||||
total = sum(len(v) for v in all_symbols.values())
|
||||
print(f"\nTotal: {total} symbols across {len(all_symbols)} libraries")
|
||||
|
||||
table = build_component_table(all_symbols)
|
||||
changed = update_readme(table)
|
||||
|
||||
if changed:
|
||||
print("\nREADME.md component index updated.")
|
||||
else:
|
||||
print("\nREADME.md already up to date.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user