#!/usr/bin/env python3 """Fill the reference template into ONE master workbook (Components_Master.xlsx). The master contains ONLY the type sheets that actually have parts (the components whose datasheets were provided) - built from a copy of the template so each kept sheet keeps its exact headers, styling, widths and freeze - plus a single **Meta** sheet appended at the END with: Template Version, Total Components, Date, Time, and a per-sheet count breakdown. Empty template sheets are dropped. Column A = __. Shared helpers (part_tag, part_to_row, template_headers, sheet_rows, read_all_rows, build_master, resolve_version, MASTER_NAME) are imported by gitea_reconcile.py so the Gitea master is built and merged the exact same way. python fill_templates.py parts.json --template --dest [--version v1] """ import argparse, json, os, re, datetime import openpyxl from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.utils import get_column_letter GREEN="B6D7A8"; GRAY="BFBFBF" MASTER_NAME="Components_Master.xlsx"; META_SHEET="Meta" 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): """Identity of a part: __. Same tag = same part.""" make=part.get("make") or make_tag(part.get("manufacturer","")) return f'{part.get("mpn","")}_{make}_{part.get("typeid","NA")}' def template_headers(template_path, ctype): """Canonical data headers for a type sheet, or None if the template has no such sheet.""" wb=openpyxl.load_workbook(template_path) if ctype not in wb.sheetnames: return None ws=wb[ctype]; return [ws.cell(1,c).value for c in range(1, ws.max_column+1)] def template_types(template_path): return [s for s in openpyxl.load_workbook(template_path).sheetnames if s!=META_SHEET] 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=ALIAS.get(norm(h),norm(h)) 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 sheet_rows(ws, width): """Existing data rows in a sheet -> (headers, {tag: {header: value}}), bounded to `width`.""" headers=[ws.cell(1,c).value for c in range(1,width+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,width+1)} return headers, rows def read_all_rows(master_path, template_path): """Read every type sheet of an existing master -> {ctype: {tag: rowdict}} (skips Meta).""" wb=openpyxl.load_workbook(master_path); types=set(template_types(template_path)); out={} for name in wb.sheetnames: if name not in types: continue th=template_headers(template_path,name); w=len(th) if th else 0 if not w: continue _,rows=sheet_rows(wb[name], w) if rows: out[name]=rows return out def _thin(): s=Side(style="thin",color=GRAY); return Border(left=s,right=s,top=s,bottom=s) def _hdr(cell): cell.font=Font(name="Calibri",bold=True); cell.fill=PatternFill("solid",fgColor=GREEN) cell.border=_thin(); cell.alignment=Alignment(horizontal="center",vertical="center") def _write_rows(ws, headers, rows_by_tag): for r in range(2, ws.max_row+1): for c in range(1, len(headers)+1): ws.cell(r,c).value=None 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 def _build_meta_sheet(wb, version, counts, when): if META_SHEET in wb.sheetnames: del wb[META_SHEET] ws=wb.create_sheet(META_SHEET) # appended at the end for r,(k,v) in enumerate([("Template Version",version), ("Total Components",sum(counts.values())), ("Date",when.strftime("%Y-%m-%d")), ("Time",when.strftime("%H:%M:%S"))], start=1): a=ws.cell(r,1,k); a.font=Font(name="Calibri",bold=True) a.fill=PatternFill("solid",fgColor=GREEN); a.border=_thin() ws.cell(r,2,v).border=_thin() r=6 for c,t in ((1,"Sheet"),(2,"Components")): _hdr(ws.cell(r,c,t)) for ctype,n in counts.items(): r+=1; ws.cell(r,1,ctype).border=_thin(); ws.cell(r,2,n).border=_thin() ws.column_dimensions["A"].width=20; ws.column_dimensions["B"].width=16 return ws 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 build_master(template_path, rows_by_type, version, dest_dir): """Write the single master workbook from a copy of the template. `rows_by_type` = {ctype: {tag: rowdict}} is the COMPLETE desired contents. Only sheets present (with at least one row) are kept; all other template sheets are dropped. A Meta sheet is appended last. Returns the output path.""" os.makedirs(dest_dir, exist_ok=True) wb=openpyxl.load_workbook(template_path); when=datetime.datetime.now(); counts={} for ctype in template_types(template_path): ws=wb[ctype]; rows=rows_by_type.get(ctype) or {} if rows: th=template_headers(template_path,ctype); width=len(th) if th else ws.max_column _write_rows(ws, [ws.cell(1,c).value for c in range(1,width+1)], rows) counts[ctype]=len(rows) _build_meta_sheet(wb, version, counts, when) # append first so a sheet always remains for ctype in template_types(template_path): if ctype not in counts: del wb[ctype] # drop empty type sheets out=os.path.join(dest_dir, MASTER_NAME); wb.save(out); return out 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); by={} for p in data.get("parts", []): headers=template_headers(a.template, p["type"]) if headers is None: print(f"! no template sheet for type '{p['type']}' - skipping"); continue tag,row=part_to_row(p, headers); by.setdefault(p["type"],{})[tag]=row out=build_master(a.template, by, version, a.dest) total=sum(len(v) for v in by.values()) print(f"master: {out} ({total} part(s) across {len(by)} sheet(s) + Meta, template {version})") if __name__=="__main__": main()