#!/usr/bin/env python3 """Shared helpers for the library-manager skill. One place for the three things every script needs to agree on: - the taxonomy (typeid -> Class -> library-repo folder name), read from ``assets/template/Type_ID.xlsx`` so there is a single source of truth; - the **per-typeid version store** (``assets/template/versions.json``) that holds each typeid's own ``template_version`` and ``skill_version`` - both start at 1 and both bump together the moment a parameter is added to that typeid (and nothing else moves); - the part **tag** ``__`` that names every folder and column-A cell. Keeping these here means fill_templates / append_parameter / gitea_components can't drift apart on how a version is read or a folder is named. """ import json, os, re import openpyxl SKILL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATE_DIR = os.path.join(SKILL_ROOT, "assets", "template") TEMPLATE_XLSX= os.path.join(TEMPLATE_DIR, "template.xlsx") TYPE_ID_XLSX = os.path.join(TEMPLATE_DIR, "Type_ID.xlsx") VERSIONS_JSON= os.path.join(TEMPLATE_DIR, "versions.json") CHANGELOG_XLSX = os.path.join(SKILL_ROOT, "assets", "CHANGELOG.xlsx") # Fixed template columns that live on every sheet and are NOT extracted from a datasheet. COL_TAG = "MPN_make_type" # column A header (the part tag lives here) COL_SKILL_VER = "Skill Version" # column B (per-typeid skill version, stamped by us) COL_TEMPLATE_VER = "Template Version" # column C (per-typeid template version, stamped by us) DESIGN_COLS = ("Library Ref", "Library Path", "Footprint Ref", "Footprint Path") # Raw taxonomy Class name -> clean, filesystem-safe folder name for the library repo. # These match Vecmocon's established sheet-name convention (the old 18-class template). CLASS_FOLDER = { "Resistor": "Resistor", "Capacitor": "Capacitor", "Inductor / Magnetics": "Inductor_Magnetics", "Diode": "Diode", "Transistor": "Transistor", "Integrated Circuit (IC)": "IC", "Protection Device": "Protection", "Power Conversion Module": "Power Conversion Module", "Relay / Contactor": "Relay_Contactor", "Switch / Button": "Switch_Button", "Connector": "Connector", "Antenna / RF": "Antenna_RF", "Crystal / Oscillator / Timing": "Crystal_Oscillator", "Battery / Cell": "Battery_Cell", "Audible / Indicator": "Audible_Indicator", "Display / HMI": "Display_HMI", "Sensor (discrete / module)": "Sensor", "Thermal / Cooling": "Thermal_Cooling", } def norm(s): return re.sub(r"[^a-z0-9]", "", str(s).lower()) # ---------------------------------------------------------------- taxonomy def load_taxonomy(type_id_xlsx=TYPE_ID_XLSX): """Return {typeid: {"class": rawclass, "subclass": name, "folder": foldername}}. Reads the Type_ID.xlsx. A real mapping row has all three of Class / Subclass / Type ID; the group-header rows (only a Class label like 'Resistor (12 types)') are skipped because they have no Type ID. """ wb = openpyxl.load_workbook(type_id_xlsx, read_only=True, data_only=True) ws = wb[wb.sheetnames[0]] out = {} for row in ws.iter_rows(min_row=1, values_only=True): cells = (list(row) + [None, None, None])[:3] cls, sub, tid = (str(c).strip() if c is not None else "" for c in cells) if not tid or tid.lower() in ("type id",): continue folder = CLASS_FOLDER.get(cls, re.sub(r"\s*/\s*", "_", re.sub(r"\s*\(.*?\)", "", cls)).strip().replace(" ", "_")) out[tid] = {"class": cls, "subclass": sub, "folder": folder} return out def class_folder(typeid, taxonomy=None): """The library-repo top-level folder name for a typeid (e.g. SCH -> 'Diode').""" tax = taxonomy or load_taxonomy() info = tax.get(typeid) return info["folder"] if info else None # ---------------------------------------------------------------- part tag def make_tag(manufacturer): """The 'make' token = first word of the manufacturer, stripped to alphanumerics.""" 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 mpn_make_prefix(mpn, make): """The __ prefix used for the early duplicate check (typeid not yet known).""" return f"{mpn}_{make}_" # ---------------------------------------------------------------- version store def _load_versions(): if os.path.exists(VERSIONS_JSON): try: return json.load(open(VERSIONS_JSON, encoding="utf-8")) except Exception: pass return {} def get_versions(typeid): """Return {'template_version': int, 'skill_version': int} for a typeid (default 1/1).""" v = _load_versions().get(typeid) or {} return {"template_version": int(v.get("template_version", 1)), "skill_version": int(v.get("skill_version", 1))} def version_labels(typeid): """Return ('vN', 'vN') = (skill_version, template_version) strings for stamping cells.""" v = get_versions(typeid) return f'v{v["skill_version"]}', f'v{v["template_version"]}' def bump_versions(typeid): """Increment BOTH template_version and skill_version for one typeid; persist and return (old, new) each as {'template_version','skill_version'}. Only this typeid changes.""" data = _load_versions() cur = data.get(typeid) or {"template_version": 1, "skill_version": 1} old = {"template_version": int(cur.get("template_version", 1)), "skill_version": int(cur.get("skill_version", 1))} new = {"template_version": old["template_version"] + 1, "skill_version": old["skill_version"] + 1} data[typeid] = new json.dump(data, open(VERSIONS_JSON, "w", encoding="utf-8"), indent=2, sort_keys=True) return old, new def init_versions(force=False): """Seed versions.json with 1/1 for every typeid in the taxonomy (idempotent).""" data = {} if force else _load_versions() for tid in load_taxonomy(): data.setdefault(tid, {"template_version": 1, "skill_version": 1}) json.dump(data, open(VERSIONS_JSON, "w", encoding="utf-8"), indent=2, sort_keys=True) return data # ---------------------------------------------------------------- changelog (read) CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"] def read_changelog(): """All changelog rows as dicts (empty list if the changelog doesn't exist yet).""" if not os.path.exists(CHANGELOG_XLSX): return [] wb = openpyxl.load_workbook(CHANGELOG_XLSX) ws = wb["Changelog"] if "Changelog" in wb.sheetnames else wb.active out = [] for r in ws.iter_rows(min_row=2, values_only=True): if not r or all(c is None for c in r): continue r = list(r) + [None] * (5 - len(r)) out.append({"date": r[0], "typeid": r[1], "skill": r[2], "template": r[3], "description": r[4]}) return out def changelog_for_typeid(typeid): """Every recorded version change for one typeid, oldest first — the cumulative history (v1->v2, v2->v3, ...) that each part sheet of that typeid embeds as its Version History.""" return [r for r in read_changelog() if str(r["typeid"]) == str(typeid)] if __name__ == "__main__": import sys if len(sys.argv) > 1 and sys.argv[1] == "init": d = init_versions(force="--force" in sys.argv) print(f"versions.json seeded with {len(d)} typeids at {VERSIONS_JSON}") else: tax = load_taxonomy() print(f"taxonomy: {len(tax)} typeids, {len(set(v['folder'] for v in tax.values()))} class folders")