Add find bottom termination parts by description
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
166
find_bottom_termination_parts.py
Normal file
166
find_bottom_termination_parts.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
From a spreadsheet (designator + description column, data from row 10), find components
|
||||
whose description indicates bottom termination package type (e.g. QFN, DFN, BGA).
|
||||
Only the description column is searched (no separate package column).
|
||||
|
||||
Usage:
|
||||
python3 find_bottom_termination_parts.py sheet.xlsx --description-col 1 [-o output.json]
|
||||
python3 find_bottom_termination_parts.py # uses SHEET, DESCRIPTION_COL, BOTTOM_TERM_OUTPUT from .env
|
||||
|
||||
All paths and options: use .env or CLI; CLI overrides .env.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
print("Install pandas and openpyxl: pip install pandas openpyxl", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Bottom-termination package patterns (case-insensitive, word boundary where needed)
|
||||
BOTTOM_TERMINATION_PATTERNS = [
|
||||
r"\bqfn\b", # Quad Flat No-leads
|
||||
r"\bdfn\b", # Dual Flat No-leads
|
||||
r"\bbga\b", # Ball Grid Array
|
||||
r"\blga\b", # Land Grid Array
|
||||
r"\bson\b", # Small Outline No-lead
|
||||
r"\bmlf\b", # Micro Leadframe
|
||||
r"\bmlp\b",
|
||||
r"\bwdfn\b",
|
||||
r"\bwqfn\b",
|
||||
r"\bvqfn\b",
|
||||
r"\buqfn\b",
|
||||
r"\bxqfn\b",
|
||||
r"\b bottom\s+termination\b", # generic + 0201/0402
|
||||
]
|
||||
BOTTOM_TERM_REGEXES = [re.compile(p, re.I) for p in BOTTOM_TERMINATION_PATTERNS]
|
||||
|
||||
|
||||
def load_sheet(
|
||||
path: str,
|
||||
designator_col: int = 0,
|
||||
description_col: int = 1,
|
||||
start_row: int = 9,
|
||||
) -> list[dict]:
|
||||
"""Load spreadsheet; return list of {designator, description} from start_row (0-based; 9 = row 10)."""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
raise FileNotFoundError(path)
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in (".xlsx", ".xls"):
|
||||
df = pd.read_excel(path, header=None, engine="openpyxl" if suffix == ".xlsx" else None)
|
||||
elif suffix == ".csv":
|
||||
df = pd.read_csv(path, header=None)
|
||||
else:
|
||||
raise ValueError(f"Unsupported format: {suffix}")
|
||||
if max(designator_col, description_col) >= df.shape[1]:
|
||||
raise ValueError(f"Sheet has {df.shape[1]} columns; need at least {max(designator_col, description_col) + 1}")
|
||||
rows = []
|
||||
for i in range(start_row, len(df)):
|
||||
des = str(df.iloc[i, designator_col]).strip() if pd.notna(df.iloc[i, designator_col]) else ""
|
||||
desc = str(df.iloc[i, description_col]).strip() if pd.notna(df.iloc[i, description_col]) else ""
|
||||
if des or desc:
|
||||
rows.append({"designator": des, "description": desc})
|
||||
return rows
|
||||
|
||||
|
||||
def is_bottom_termination_in_description(description: str) -> tuple[bool, str]:
|
||||
"""
|
||||
True if description (case-insensitive) contains a bottom termination package type
|
||||
(e.g. QFN, DFN, BGA). Returns (matched, pattern_matched) e.g. (True, "qfn").
|
||||
"""
|
||||
if not (description or "").strip():
|
||||
return False, ""
|
||||
d = description.lower()
|
||||
for pat in BOTTOM_TERM_REGEXES:
|
||||
if pat.search(d):
|
||||
name = pat.pattern.replace(r"\b", "").replace("\\s+", " ").strip()
|
||||
return True, name
|
||||
return False, ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
default_sheet = os.environ.get("SHEET", "").strip() or None
|
||||
default_out = os.environ.get("BOTTOM_TERM_OUTPUT", "").strip() or "output/bottom_termination_parts.json"
|
||||
default_des_col = os.environ.get("DESCRIPTION_COL", "").strip()
|
||||
default_start = os.environ.get("START_ROW", "").strip()
|
||||
try:
|
||||
default_des_col = int(default_des_col) if default_des_col else 1
|
||||
except ValueError:
|
||||
default_des_col = 1
|
||||
try:
|
||||
default_start = int(default_start) if default_start else 9
|
||||
except ValueError:
|
||||
default_start = 9
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Find components with bottom termination package types (e.g. QFN, DFN, BGA); only description column is searched"
|
||||
)
|
||||
parser.add_argument("file", nargs="?", default=default_sheet, help="Spreadsheet path (default: SHEET from .env)")
|
||||
parser.add_argument("-o", "--output", default=default_out, help="Output JSON (default: BOTTOM_TERM_OUTPUT from .env)")
|
||||
parser.add_argument("--designator-col", type=int, default=0, help="Designator column 0-based (default 0)")
|
||||
parser.add_argument("--description-col", type=int, default=default_des_col, metavar="COL", help="Description column 0-based (searched for package types; default: DESCRIPTION_COL from .env or 1)")
|
||||
parser.add_argument("--start-row", type=int, default=default_start, help="First data row 0-based, 9=row 10 (default: START_ROW from .env or 9)")
|
||||
args = parser.parse_args()
|
||||
|
||||
path = (args.file or default_sheet or "").strip()
|
||||
if not path:
|
||||
parser.error("No spreadsheet. Set SHEET in .env or pass file path.")
|
||||
if not Path(path).exists():
|
||||
print(f"Error: file not found: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
rows = load_sheet(
|
||||
path,
|
||||
args.designator_col,
|
||||
args.description_col,
|
||||
args.start_row,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
matches = []
|
||||
for r in rows:
|
||||
ok, pattern = is_bottom_termination_in_description(r["description"])
|
||||
if ok:
|
||||
matches.append({**r, "matched_pattern": pattern})
|
||||
|
||||
report = {
|
||||
"file": path,
|
||||
"designator_col": args.designator_col,
|
||||
"description_col": args.description_col,
|
||||
"start_row": args.start_row + 1,
|
||||
"count": len(matches),
|
||||
"parts": matches,
|
||||
}
|
||||
|
||||
out_path = Path(args.output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"Wrote {args.output}")
|
||||
print(f"Found {len(matches)} components with bottom termination (by description column)")
|
||||
for m in matches:
|
||||
extra = f" [{m['matched_pattern']}]" if m.get("matched_pattern") else ""
|
||||
desc = m["description"][:70] + "..." if len(m["description"]) > 70 else m["description"]
|
||||
print(f" {m['designator']}: {desc}{extra}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user