164 lines
6.7 KiB
Python
164 lines
6.7 KiB
Python
#!/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 -> components-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** ``<MPN>_<make>_<typeid>`` 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_MD = os.path.join(TEMPLATE_DIR, "CHANGELOG.md")
|
|
|
|
# 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 components 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 components-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: <MPN>_<make>_<typeid>. 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 <MPN>_<make>_ 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
|
|
|
|
|
|
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")
|