#!/usr/bin/env python3 """ BOM CSV Exporter ================ Reads extract_symbols.json and writes bom.csv with columns: Reference, MPN, MFG Power symbols (#PWR, #FLG) are excluded. Usage: python3 export_bom.py [project_dir] """ import csv import json import sys from pathlib import Path def main(project_dir: Path): json_path = project_dir / 'extract_symbols.json' if not json_path.exists(): print(f"Error: {json_path} not found. Run extract_symbols.py first.", file=sys.stderr) sys.exit(1) data = json.loads(json_path.read_text(encoding='utf-8')) symbols = data['symbols'] # Filter out power/flag symbols, DNP parts, and parts excluded from BOM parts = [ s for s in symbols if not (s.get('reference') or '').startswith(('#', '~')) and not (s.get('lib_id') or '').startswith('power:') and s.get('in_bom') is not False and not s.get('dnp') ] # Collapse multi-unit / duplicate records to one row per reference. # If multiple records exist for the same ref, pick the one with the # most complete MPN/MFG data (longest non-placeholder string). def data_score(s): props = s.get('properties', {}) mpn = props.get('MPN', '') mfg = props.get('MFG') or props.get('MANUFACTURER', '') placeholder = mpn.strip().lower() in ('', 'x', 'tbd', 'n/a', 'na') return (0 if placeholder else len(mpn) + len(mfg)) from collections import defaultdict by_ref: dict[str, list] = defaultdict(list) for s in parts: by_ref[s.get('reference', '')].append(s) best: list[dict] = [] for ref, recs in by_ref.items(): best.append(max(recs, key=data_score)) # Sort by reference def ref_sort_key(r): ref = r.get('reference') or '' letters = ''.join(c for c in ref if c.isalpha()) digits = ''.join(c for c in ref if c.isdigit()) return (letters, int(digits) if digits else 0) best.sort(key=ref_sort_key) out_path = project_dir / 'bom.csv' with out_path.open('w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['Reference', 'MPN', 'MFG']) for s in best: props = s.get('properties', {}) writer.writerow([ s.get('reference', ''), props.get('MPN', ''), props.get('MFG') or props.get('MANUFACTURER', ''), ]) excluded_bom = sum(1 for s in symbols if s.get('in_bom') is False and not (s.get('reference') or '').startswith(('#', '~'))) excluded_dnp = sum(1 for s in symbols if s.get('dnp') and not (s.get('reference') or '').startswith(('#', '~')) and s.get('in_bom') is not False) print(f"Excluded: {excluded_bom} 'exclude from BOM', {excluded_dnp} DNP") print(f"Wrote {len(best)} unique references to {out_path} (collapsed from {len(parts)} records)") if __name__ == '__main__': project_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).parent.resolve() main(project_dir)