updated .py script
This commit is contained in:
@@ -385,7 +385,7 @@ AidanBrzezinski_KiCad_Library/
|
|||||||
|
|
||||||
| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |
|
| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| 74LS192 | Synchronous 4-bit Up/Down (2 clk) counter | - | 0_ic_logic | - | - |
|
| SN74HC193DR | Synchronous 4-bit Up/Down (2 clk) counter | - | 0_ic_logic | 0_package_SO:SOIC-16_3.9x9.9mm_P1.27mm | - |
|
||||||
|
|
||||||
*1 components — auto-generated 2026-03-08*
|
*1 components — auto-generated 2026-03-08*
|
||||||
|
|
||||||
|
|||||||
@@ -9,33 +9,20 @@ Reads the following fields from each symbol:
|
|||||||
Symbol Library column = which .kicad_sym file the symbol lives in.
|
Symbol Library column = which .kicad_sym file the symbol lives in.
|
||||||
Used to locate a symbol quickly in KiCad Symbol Editor.
|
Used to locate a symbol quickly in KiCad Symbol Editor.
|
||||||
|
|
||||||
|
Handles derived symbols (Derive from symbol) by merging parent fields
|
||||||
|
with child overrides so all fields resolve correctly.
|
||||||
|
|
||||||
Run locally: python3 .github/scripts/update_component_index.py
|
Run locally: python3 .github/scripts/update_component_index.py
|
||||||
Run in CI: triggered automatically on push to symbols/
|
Run in CI: triggered automatically on push to symbols/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
SYMBOLS_DIR = Path("symbols")
|
SYMBOLS_DIR = Path("symbols")
|
||||||
README_PATH = Path("README.md")
|
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 = [
|
CATEGORY_ORDER = [
|
||||||
"0_ic_logic",
|
"0_ic_logic",
|
||||||
"0_ic_driver",
|
"0_ic_driver",
|
||||||
@@ -50,63 +37,96 @@ CATEGORY_ORDER = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def parse_field(symbol_block: str, field_name: str) -> str:
|
def extract_all_fields(block: str) -> dict:
|
||||||
"""Extract a named field value from a symbol block."""
|
"""Extract all property fields from a symbol block into a dict."""
|
||||||
# KiCad sym format: (property "FieldName" "Value" ...)
|
fields = {}
|
||||||
pattern = rf'\(property\s+"{re.escape(field_name)}"\s+"([^"]*)"'
|
for m in re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]*)"', block):
|
||||||
match = re.search(pattern, symbol_block)
|
fields[m.group(1)] = m.group(2).strip()
|
||||||
return match.group(1).strip() if match else ""
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def parse_symbols_from_file(filepath: Path) -> list[dict]:
|
def shorten_footprint(footprint: str) -> str:
|
||||||
"""
|
"""Trim long footprint names for table readability."""
|
||||||
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:
|
if footprint and ":" in footprint:
|
||||||
lib_part, fp_name = footprint.split(":", 1)
|
lib_part, fp_name = footprint.split(":", 1)
|
||||||
# Truncate very long footprint names
|
|
||||||
if len(fp_name) > 30:
|
if len(fp_name) > 30:
|
||||||
fp_name = fp_name[:27] + "..."
|
fp_name = fp_name[:27] + "..."
|
||||||
footprint = f"{lib_part}:{fp_name}"
|
return f"{lib_part}:{fp_name}"
|
||||||
|
return footprint
|
||||||
|
|
||||||
|
|
||||||
|
def parse_symbols_from_file(filepath: Path) -> list:
|
||||||
|
"""
|
||||||
|
Parse all symbols from a .kicad_sym file.
|
||||||
|
|
||||||
|
Handles both normal symbols and derived symbols (Derive from symbol).
|
||||||
|
Derived symbols only store fields that differ from their parent — the
|
||||||
|
parser merges parent fields first then overlays child overrides so all
|
||||||
|
fields resolve correctly.
|
||||||
|
"""
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# --- Pass 1: collect ALL raw symbol blocks indexed by name ---
|
||||||
|
symbol_pattern = re.compile(r'\(symbol\s+"([^"]+)"', re.MULTILINE)
|
||||||
|
all_matches = list(symbol_pattern.finditer(content))
|
||||||
|
|
||||||
|
raw_blocks = {} # name -> raw text block
|
||||||
|
extends_map = {} # child name -> parent name
|
||||||
|
|
||||||
|
for i, match in enumerate(all_matches):
|
||||||
|
name = match.group(1)
|
||||||
|
# Skip KiCad internal sub-unit blocks e.g. SN74HC193DR_0_1
|
||||||
|
if re.search(r'_\d+_\d+$', name):
|
||||||
|
continue
|
||||||
|
start = match.start()
|
||||||
|
end = all_matches[i + 1].start() if i + 1 < len(all_matches) else len(content)
|
||||||
|
block = content[start:end]
|
||||||
|
raw_blocks[name] = block
|
||||||
|
|
||||||
|
extends_match = re.search(r'\(extends\s+"([^"]+)"\)', block)
|
||||||
|
if extends_match:
|
||||||
|
extends_map[name] = extends_match.group(1)
|
||||||
|
|
||||||
|
# --- Pass 2: resolve fields, merging parent into child ---
|
||||||
|
def resolve_fields(name, visited=None):
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
if name in visited:
|
||||||
|
return {}
|
||||||
|
visited.add(name)
|
||||||
|
block = raw_blocks.get(name, "")
|
||||||
|
own_fields = extract_all_fields(block)
|
||||||
|
parent_name = extends_map.get(name)
|
||||||
|
if parent_name:
|
||||||
|
parent_fields = resolve_fields(parent_name, visited)
|
||||||
|
return {**parent_fields, **own_fields}
|
||||||
|
return own_fields
|
||||||
|
|
||||||
|
# --- Pass 3: build symbol list ---
|
||||||
|
symbols = []
|
||||||
|
|
||||||
|
for name in raw_blocks:
|
||||||
|
is_derived = name in extends_map
|
||||||
|
fields = resolve_fields(name)
|
||||||
|
mpn = fields.get("MPN") or fields.get("Value") or name
|
||||||
|
|
||||||
|
# Skip base/template symbols — no MPN field, just drawing sources
|
||||||
|
if not is_derived and not fields.get("MPN"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
description = (fields.get("ki_description")
|
||||||
|
or fields.get("Description")
|
||||||
|
or fields.get("description")
|
||||||
|
or "—")
|
||||||
|
footprint = shorten_footprint(fields.get("Footprint", "")) or "-"
|
||||||
|
digikey_pn = fields.get("Digikey_PN") or fields.get("Digikey PN") or "-"
|
||||||
|
manufacturer = fields.get("Manufacturer") or "-"
|
||||||
|
|
||||||
symbols.append({
|
symbols.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"mpn": mpn,
|
"mpn": mpn,
|
||||||
"description": description,
|
"description": description,
|
||||||
"footprint": footprint or "-",
|
"footprint": footprint,
|
||||||
"digikey_pn": digikey_pn,
|
"digikey_pn": digikey_pn,
|
||||||
"manufacturer": manufacturer,
|
"manufacturer": manufacturer,
|
||||||
})
|
})
|
||||||
@@ -114,7 +134,7 @@ def parse_symbols_from_file(filepath: Path) -> list[dict]:
|
|||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
|
|
||||||
def build_component_table(all_symbols: dict[str, list[dict]]) -> str:
|
def build_component_table(all_symbols: dict) -> str:
|
||||||
"""Build the full markdown component index table."""
|
"""Build the full markdown component index table."""
|
||||||
lines = []
|
lines = []
|
||||||
lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |")
|
lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |")
|
||||||
@@ -128,7 +148,7 @@ def build_component_table(all_symbols: dict[str, list[dict]]) -> str:
|
|||||||
for s in sorted(symbols, key=lambda x: x["mpn"]):
|
for s in sorted(symbols, key=lambda x: x["mpn"]):
|
||||||
lines.append(
|
lines.append(
|
||||||
f"| {s['mpn']} "
|
f"| {s['mpn']} "
|
||||||
f"| {s['description'] or '—'} "
|
f"| {s['description']} "
|
||||||
f"| {s['manufacturer']} "
|
f"| {s['manufacturer']} "
|
||||||
f"| {category} "
|
f"| {category} "
|
||||||
f"| {s['footprint']} "
|
f"| {s['footprint']} "
|
||||||
@@ -143,13 +163,11 @@ def build_component_table(all_symbols: dict[str, list[dict]]) -> str:
|
|||||||
|
|
||||||
def update_readme(new_table: str) -> bool:
|
def update_readme(new_table: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Replace the content between the Component Index header and the next
|
Replace the Component Index section in README.md with the new table.
|
||||||
top-level ## header in README.md with the new table.
|
|
||||||
Returns True if the file was changed.
|
Returns True if the file was changed.
|
||||||
"""
|
"""
|
||||||
readme = README_PATH.read_text(encoding="utf-8")
|
readme = README_PATH.read_text(encoding="utf-8")
|
||||||
|
|
||||||
# Match from ## Component Index to the next ## section or end of file
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r'(## Component Index\n+)(.*?)(\n## |\Z)',
|
r'(## Component Index\n+)(.*?)(\n## |\Z)',
|
||||||
re.DOTALL
|
re.DOTALL
|
||||||
@@ -178,14 +196,14 @@ def update_readme(new_table: str) -> bool:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
if not SYMBOLS_DIR.exists():
|
if not SYMBOLS_DIR.exists():
|
||||||
print(f"ERROR: symbols/ directory not found. Run from repo root.")
|
print("ERROR: symbols/ directory not found. Run from repo root.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not README_PATH.exists():
|
if not README_PATH.exists():
|
||||||
print(f"ERROR: README.md not found. Run from repo root.")
|
print("ERROR: README.md not found. Run from repo root.")
|
||||||
return
|
return
|
||||||
|
|
||||||
all_symbols: dict[str, list[dict]] = {}
|
all_symbols = {}
|
||||||
|
|
||||||
for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")):
|
for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")):
|
||||||
lib_name = sym_file.stem
|
lib_name = sym_file.stem
|
||||||
|
|||||||
Reference in New Issue
Block a user