Remove Altium 365 Part Request end task (skill ends at Gitea push); schlib_write.py: source full params + Description from the verified per-part xlsx via --from-xlsx

main
admin 2026-07-16 08:18:50 +00:00
parent 2719f5c75b
commit 6083e4ee78
No known key found for this signature in database
3 changed files with 74 additions and 80 deletions

View File

@ -341,15 +341,27 @@ empty until filled. The full method for the second-source search is in
`references/schlib_parameters.md`. `references/schlib_parameters.md`.
Write the **full parameter set** — the typeid template's engineering columns **plus** the SOP Write the **full parameter set** — the typeid template's engineering columns **plus** the SOP
params above (see `references/schlib_parameters.md`). Collect your filled values into a params above (see `references/schlib_parameters.md`) — and **source it from the verified per-part
`params.json` and pass `--typeid` so the writer guarantees every template column is present Excel** so the symbol and the workbook can never disagree. After the sheet is verified (step 5),
(blank where the datasheet is silent): pass that `<tag>.xlsx` to the writer with `--from-xlsx`: every engineering column **and the
Description** are read straight out of it and written into the `.SchLib`. Then layer on the
**SOP-only** fields that aren't template columns (the `Value` shorthand, `Manufacturer Part`,
`Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the blank
second-source pair) via a small `params.json`; on any name collision the `params.json` value
wins. Pass `--typeid` too, so any template column the datasheet left silent is still present
(blank):
```bash ```bash
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json \ python scripts/schlib_write.py --schlib <in>.SchLib \
--from-xlsx <stage>/<tag>/<tag>.xlsx --params params.json \
--out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID> --out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID>
``` ```
(`--params` is optional if the Excel already carries everything you need; `--from-xlsx` is
optional if you'd rather hand-build the whole set in `params.json` — give at least one.) The
`Description` written onto the symbol (and into the component's ComponentDescription field) is
the exact one from the Excel, which was built to `references/description_format.md`.
Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then
**Saves to Server** with a revision note. The full parameter set, each value's source, the **Saves to Server** with a revision note. The full parameter set, each value's source, the
`params.json` shape (incl. the `remove` list), and the mini-stream size caveat are in `params.json` shape (incl. the `remove` list), and the mini-stream size caveat are in
@ -358,61 +370,6 @@ round-trip, fall back to `scripts/altium_params.py` (emits an Altium DXP script
parameters from inside Altium). Always have the engineer confirm the file opens in Altium — the parameters from inside Altium). Always have the engineer confirm the file opens in Altium — the
skill writes Altium's own binary format, so Altium is the final validator. skill writes Altium's own binary format, so Altium is the final validator.
## Submitting to Altium 365 as Part Requests (web)
If the org's central library is a managed **Altium 365 Workspace** (not the Gitea repos), the
skill's end task can submit each finished component as a **Part Request** through the Workspace
web UI, using browser automation (Claude-in-Chrome) in the operator's own signed-in Chrome — no
API token, no admin rights. A librarian then approves each request into the library.
This runs over **every component processed in the run**: the skill writes a `part_requests.json`
manifest (one entry per component — manufacturer, MPN, Description, component type, the full
parameter set, and the local paths to its `.SchLib`/`.PcbLib`/datasheet), then the browser step
loops it, filling and submitting the form for each. On the **first** component, fill everything
and stop at Save for the operator to review; once confirmed, Save and loop the rest, logging each
Request Id. Full field mapping, prerequisites, and the exact browser steps are in
`references/part_request_web.md` — read it before driving the browser.
Because the files upload from local disk, commit each component's `.SchLib`/`.PcbLib`/datasheet
to the operator's machine (device bridge) first, and put those local paths in the manifest.
### The end-to-end workflow (this is the standard run)
When the central library is a managed Altium 365 Workspace, a full run goes:
1. **Datasheet + make** → extract parameters, classify to a typeid, fill the per-part Excel
workbook, human-verify (the existing flow).
2. **Operator provides the `.SchLib` + `.PcbLib`** → write the full parameter set into the
`.SchLib` with `schlib_write.py --typeid <ID>` (template columns + SOP params, plus the
ComponentDescription), exactly as before.
3. **Push to Gitea** (part folder + symbol/footprint, per the normal push steps).
4. **Prepare the Altium 365 Part Request and hand it to the local runner** — the automatic tail:
a. Build the manifest:
```bash
python scripts/build_part_request_manifest.py --params <part params.json> --typeid <ID> \
--file <inbox>/<MPN>.SchLib --file <inbox>/<MPN>.PcbLib --file <inbox>/<MPN>_datasheet.pdf \
--out <inbox>/part_requests.json
```
(`build_part_request_manifest.py` maps the typeid to the Workspace Component Type — e.g.
`CER → Capacitors` — pulls manufacturer/MPN/Description/parameters from the params, and lists
the local attachment paths.)
b. **Commit the `.SchLib`, `.PcbLib`, datasheet, and `part_requests.json` into the operator's
Altium Runner inbox** on their PC via the device bridge (the `inbox` folder in
`runner_config.json`, e.g. `C:/Altium Runner/inbox`). The manifest's `files` paths must be
those on-disk inbox paths.
5. **The local runner finishes it, hands-off.** `scripts/local_runner.py` (installed once on the
operator's machine as a Task Scheduler job at logon) watches the inbox, ensures Chrome is up
with the debug port on the signed-in profile, and runs `altium365_part_request.py`, which
**fills the Part Request completely — fields, parameters, and all three attachments — and
leaves it on screen without saving** (review mode). The operator just **reviews and clicks
Save**. Nothing is submitted without their click.
So the operator's only actions per component are giving the datasheet+make, then the
`.SchLib`+`.PcbLib`, then reviewing the filled Part Request and clicking Save. Everything between
is automatic, and the submission step costs zero Claude tokens. Standing requirements: the
operator's machine is on, the runner is installed, and its Chrome profile has been signed into the
Workspace once (the runner relaunches Chrome with the debug port and reuses the session).
## Per-typeid versioning ## Per-typeid versioning
Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and
@ -572,20 +529,6 @@ plain flat push, but it does not merge the changelog or blank the token, so pref
- `references/schlib_parameters.md` — the SOP **mandatory symbol parameters** (§5) for the - `references/schlib_parameters.md` — the SOP **mandatory symbol parameters** (§5) for the
`.SchLib`: the parameter set, where each value comes from, and how the generated Altium script `.SchLib`: the parameter set, where each value comes from, and how the generated Altium script
stamps them onto the symbol. stamps them onto the symbol.
- `references/part_request_web.md` — submitting finished components to a managed Altium 365
Workspace as **Part Requests** (no token/admin): prerequisites, per-component field mapping,
the `part_requests.json` manifest, the captured form selectors, and both routes (live
Claude-in-Chrome, or the standalone Selenium script).
- `scripts/altium365_part_request.py` — standalone **Selenium** submitter: attaches to the
operator's signed-in Chrome and loops the `part_requests.json` manifest, filling and saving each
Part Request. Token-free per component; the token-economical end task for a whole library.
- `scripts/build_part_request_manifest.py` — build/append `part_requests.json` from a component's
schlib `params.json` + local file paths; maps typeid → Altium Component Type. Run at the end of
a run, before committing the manifest+files to the operator's inbox.
- `scripts/local_runner.py` (+ `runner_config.example.json`) — the operator installs this **once**
on their machine; it watches the inbox for manifests the skill drops there, ensures Chrome's
debug session, and runs the Selenium submitter in **review mode** (fills everything, leaves the
form for the operator to review + Save) — the automatic, hands-off tail of every run.
- `assets/template/versions.json` — per-typeid `template_version` + `skill_version`. - `assets/template/versions.json` — per-typeid `template_version` + `skill_version`.
- `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add; - `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
merged into the skill repo's copy in Gitea by `push-skill`). merged into the skill repo's copy in Gitea by `push-skill`).

Binary file not shown.

View File

@ -18,8 +18,18 @@ What it does to the target component's Data stream:
- leaves pins, graphics, the Comment and all other records exactly as they were. - leaves pins, graphics, the Comment and all other records exactly as they were.
Usage: Usage:
# source parameters + Description from the verified per-part workbook (preferred):
python schlib_write.py --schlib IN.SchLib --from-xlsx <tag>.xlsx \
[--params sop.json] --typeid ELE --out OUT.SchLib
# or from a params.json alone:
python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib
--from-xlsx reads the filled per-part `<tag>.xlsx` (the same workbook the engineer verified) and
writes every engineering column + the Description into the symbol, so the .SchLib and the sheet
never diverge. --params then layers on the SOP-only fields that aren't template columns
(Value shorthand, Process, ROHS, Datasheet, Manufacturer Part, Vecmocon Part Code, ...); on a
name collision the params.json value wins. Give either or both (at least one).
params.json (same shape altium_params.py uses): params.json (same shape altium_params.py uses):
{"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps {"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps
"parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden", ...}, "parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden", ...},
@ -62,6 +72,28 @@ def template_param_names(template_path, typeid):
if ws.cell(1, c).value and ws.cell(1, c).value not in NON_PARAM_COLS] if ws.cell(1, c).value and ws.cell(1, c).value not in NON_PARAM_COLS]
def params_from_xlsx(xlsx_path, sheet=None):
"""Read the filled parameter values out of a per-part workbook (`<tag>.xlsx`) — row 1 is the
headers, row 2 is the single data row this skill writes. Returns {header: value} for every
engineering column (i.e. NOT the bookkeeping/design columns in NON_PARAM_COLS), INCLUDING the
Description column. This is what lets the .SchLib carry exactly the parameters + Description
the engineer already verified in Excel the two never diverge. Blank cells stay blank.
By default it reads the first sheet (the typeid parameter sheet). A `Version History` second
sheet, if present, is skipped."""
import openpyxl
wb = openpyxl.load_workbook(xlsx_path, read_only=True, data_only=True)
ws = wb[sheet] if sheet else wb[wb.sheetnames[0]]
headers = [ws.cell(1, c).value for c in range(1, ws.max_column + 1)]
values = [ws.cell(2, c).value for c in range(1, ws.max_column + 1)]
out = {}
for h, v in zip(headers, values):
if not h or h in NON_PARAM_COLS:
continue
out[str(h)] = "" if v is None else str(v)
return out
# ----------------------------------------------------------------- read the container # ----------------------------------------------------------------- read the container
def read_container(path): def read_container(path):
@ -249,11 +281,21 @@ def rebuild(entries, content):
# ----------------------------------------------------------------- driver # ----------------------------------------------------------------- driver
def write_params(schlib, params_json, out, typeid=None, template=None): def write_params(schlib, params_json, out, typeid=None, template=None, from_xlsx=None):
params_json = params_json or {}
component=params_json.get("component") or None component=params_json.get("component") or None
fields=dict(params_json.get("parameters", {}) or {})
remove=params_json.get("remove", DEFAULT_REMOVE) remove=params_json.get("remove", DEFAULT_REMOVE)
# Base the parameter set on the verified per-part Excel when given (--from-xlsx): every
# engineering column and the Description come straight from the workbook the engineer already
# verified, so the symbol and the sheet can never disagree. Explicit params.json entries
# (the SOP-only fields like Value/Process/ROHS/Datasheet/Vecmocon Part Code that aren't
# template columns) are layered on top and win on any name collision.
fields = {}
if from_xlsx:
fields.update(params_from_xlsx(from_xlsx))
fields.update(params_json.get("parameters", {}) or {})
# If a typeid+template are given, guarantee the FULL template parameter set is written: # If a typeid+template are given, guarantee the FULL template parameter set is written:
# every engineering column for that typeid becomes a symbol parameter (value from the # every engineering column for that typeid becomes a symbol parameter (value from the
# params if provided, else blank). This is what makes every .SchLib carry the complete, # params if provided, else blank). This is what makes every .SchLib carry the complete,
@ -299,17 +341,26 @@ def write_params(schlib, params_json, out, typeid=None, template=None):
def main(): def main():
ap=argparse.ArgumentParser() ap=argparse.ArgumentParser()
ap.add_argument("--schlib", required=True) ap.add_argument("--schlib", required=True)
ap.add_argument("--params", required=True) ap.add_argument("--params", help="params.json with SOP fields (Value/Process/ROHS/Datasheet/"
"Vecmocon Part Code/...) + optional 'component'/'remove'. "
"Optional if --from-xlsx supplies the parameters.")
ap.add_argument("--from-xlsx", dest="from_xlsx",
help="the verified per-part <tag>.xlsx: every engineering column + the "
"Description are written into the .SchLib straight from it, so the "
"symbol matches the sheet. Layer SOP-only fields on with --params.")
ap.add_argument("--out", required=True) ap.add_argument("--out", required=True)
ap.add_argument("--typeid", help="component typeid; with --template, writes that typeid's " ap.add_argument("--typeid", help="component typeid; with --template, guarantees that typeid's "
"full template parameter set (blank where not provided)") "full template parameter set is present (blank where absent)")
ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)") ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)")
a=ap.parse_args() a=ap.parse_args()
if not a.params and not a.from_xlsx:
ap.error("give --params and/or --from-xlsx (at least one source of parameters)")
template = a.template template = a.template
if a.typeid and not template: if a.typeid and not template:
template = os.path.join(os.path.dirname(__file__), "..", "assets", "template", "template.xlsx") template = os.path.join(os.path.dirname(__file__), "..", "assets", "template", "template.xlsx")
write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out, params_json = json.load(open(a.params, encoding="utf-8")) if a.params else {}
typeid=a.typeid, template=template) write_params(a.schlib, params_json, a.out,
typeid=a.typeid, template=template, from_xlsx=a.from_xlsx)
if __name__ == "__main__": if __name__ == "__main__":