#!/usr/bin/env python3 """Fill the master multi-type template from parts.json (or feed the merge/dedup path). Per component type present, writes one .xlsx containing the filled type sheet plus a Meta sheet (template version, date). Column A ("MPN_make_type") = __, where make = first word of the manufacturer and typeid comes from the taxonomy. The row/style helpers (part_tag, part_to_row, read_type_rows, write_type_workbook, template_headers, resolve_version) are imported by gitea_reconcile.py so the merge path produces byte-for-byte the same styling as a fresh generate - no duplicated logic. Fresh generate: python fill_templates.py parts.json --template --dest [--version v1] """ import argparse, json, os, re, datetime import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter GREEN="B6D7A8"; GRAY="BFBFBF" def norm(s): return re.sub(r'[^a-z0-9]', '', str(s).lower()) ALIAS={"induactanceuh":"inductanceuh"} # template header typo -> value key def make_tag(manufacturer): w = re.split(r'\s+', str(manufacturer).strip()) first = w[0] if w and w[0] else "NA" return re.sub(r'[^A-Za-z0-9]', '', first) or "NA" def part_tag(part): """The identity of a part: __. Same MPN tag = same part.""" make = part.get("make") or make_tag(part.get("manufacturer","")) return f'{part.get("mpn","")}_{make}_{part.get("typeid","NA")}' def style_header(cell): cell.font=Font(name="Calibri", bold=True); cell.fill=PatternFill("solid", fgColor=GREEN) s=Side(style="thin", color=GRAY); cell.border=Border(left=s,right=s,top=s,bottom=s) cell.alignment=Alignment(horizontal="center", vertical="center", wrap_text=True) def template_headers(template_path, ctype): """Canonical header list for a type sheet, or None if the template has no such sheet.""" master=openpyxl.load_workbook(template_path) if ctype not in master.sheetnames: return None ms=master[ctype] return [ms.cell(1,c).value for c in range(1, ms.max_column+1)] def part_to_row(part, headers): """Return (tag, {header: value}) for one part, following the column rules.""" vals={norm(k):v for k,v in part.get("values", {}).items()} tag=part_tag(part) row={} for c,h in enumerate(headers, start=1): key=norm(h); key=ALIAS.get(key,key) if c==1: row[h]=tag elif key=="class": row[h]=part.get("subclass","") elif key=="manufacturer":row[h]=part.get("manufacturer", vals.get("manufacturer","")) else: row[h]=vals.get(key,"") return tag, row def read_type_rows(path): """Read an existing .xlsx -> (headers, {tag: {header: value}}); Meta sheet ignored.""" wb=openpyxl.load_workbook(path) ws=next((wb[n] for n in wb.sheetnames if n!="Meta"), None) if ws is None: return [], {} headers=[ws.cell(1,c).value for c in range(1, ws.max_column+1)] rows={} for r in range(2, ws.max_row+1): tag=ws.cell(r,1).value if tag in (None,""): continue rows[tag]={headers[c-1]: ws.cell(r,c).value for c in range(1, len(headers)+1)} return headers, rows def write_type_workbook(ctype, headers, rows_by_tag, version, dest_dir): """Write one styled .xlsx (type sheet + Meta) from an ordered {tag: rowdict}. Rows are pulled by header name, so callers can pass the canonical template headers and any missing field simply comes out blank. Returns the filename written.""" os.makedirs(dest_dir, exist_ok=True) out=openpyxl.Workbook(); ws=out.active; ws.title=ctype[:31] for c,h in enumerate(headers, start=1): ws.cell(1,c,h); style_header(ws.cell(1,c)) ws.row_dimensions[1].height=30 r=2 for tag,row in rows_by_tag.items(): for c,h in enumerate(headers, start=1): ws.cell(r,c, row.get(h,"")) r+=1 for c,h in enumerate(headers, start=1): w=32 if norm(h)=="description" else (24 if c==1 else max(11,min(18,len(str(h))+2))) ws.column_dimensions[get_column_letter(c)].width=w ws.freeze_panes="A2" meta=out.create_sheet("Meta") mrows=[("Template Version", version), ("Generated", datetime.date.today().isoformat()), ("Component Type", ctype), ("Parts in file", len(rows_by_tag)), ("Source", "datasheet-extractor skill")] for i,(k,v) in enumerate(mrows, start=1): meta.cell(i,1,k).font=Font(bold=True); meta.cell(i,2,v) meta.column_dimensions["A"].width=20; meta.column_dimensions["B"].width=40 fn=re.sub(r'\s+','_',ctype)+".xlsx" out.save(os.path.join(dest_dir, fn)) return fn def resolve_version(template_path, version): if version: return version vf=os.path.join(os.path.dirname(template_path), "VERSION") return "v"+open(vf).read().strip() if os.path.exists(vf) else "v1" def main(): ap=argparse.ArgumentParser() ap.add_argument("parts_json"); ap.add_argument("--template", required=True) ap.add_argument("--dest", required=True); ap.add_argument("--version", default=None) a=ap.parse_args() data=json.load(open(a.parts_json, encoding="utf-8")) version=resolve_version(a.template, a.version) os.makedirs(a.dest, exist_ok=True) by={} for p in data.get("parts", []): by.setdefault(p["type"], []).append(p) for ctype, parts in by.items(): headers=template_headers(a.template, ctype) if headers is None: print(f"! no template sheet for type '{ctype}' - skipping"); continue rows_by_tag={} for p in parts: tag,row=part_to_row(p, headers) rows_by_tag[tag]=row fn=write_type_workbook(ctype, headers, rows_by_tag, version, a.dest) print(f"{ctype}: {len(rows_by_tag)} row(s) -> {fn} (template {version})") if __name__=="__main__": main()