Finalize Altium 365 Part Request end task: build_part_request_manifest.py (typeid->component-type), runner review-mode (fill+leave for Save), full end-to-end workflow in SKILL.md
parent
c74c9d8e22
commit
7ff9751504
57
SKILL.md
57
SKILL.md
|
|
@ -376,26 +376,42 @@ Request Id. Full field mapping, prerequisites, and the exact browser steps are i
|
|||
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.
|
||||
|
||||
### Fully automatic end-of-run (no operator action)
|
||||
### The end-to-end workflow (this is the standard run)
|
||||
|
||||
To make the Part Request the automatic tail of every run, the end task doesn't drive the browser
|
||||
from the cloud (it can't reach the operator's desktop Chrome). Instead:
|
||||
When the central library is a managed Altium 365 Workspace, a full run goes:
|
||||
|
||||
1. After the Gitea push, the skill **writes `part_requests.json`** for the component(s) processed
|
||||
this run, and **commits it plus each component's `.SchLib`/`.PcbLib`/datasheet to an *inbox
|
||||
folder on the operator's machine*** via the device bridge (e.g.
|
||||
`.../altium-library-master/_part_request_inbox/`). Use the local file paths (under that inbox)
|
||||
in the manifest's `files`.
|
||||
2. A one-time **local runner** on the operator's Windows machine (`scripts/local_runner.py`, set
|
||||
up as a Task Scheduler job at logon or a startup shortcut, configured via
|
||||
`runner_config.json`) watches that inbox, ensures Chrome is up with the debug port on the
|
||||
signed-in profile, runs `altium365_part_request.py` on the manifest, and archives it.
|
||||
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 each run ends with the skill dropping the manifest+files into the inbox; the local runner
|
||||
submits the Part Request on its own. The only standing requirements are that the operator's
|
||||
machine is on and its Chrome profile has been signed into the Workspace at least once (the runner
|
||||
relaunches Chrome with the debug port and the persisted session is reused). This keeps the
|
||||
per-component Claude-token cost at zero for the submission step.
|
||||
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
|
||||
|
||||
|
|
@ -563,10 +579,13 @@ plain flat push, but it does not merge the changelog or blank the token, so pref
|
|||
- `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, runs the Selenium submitter, and archives each manifest — making submission the
|
||||
automatic, hands-off tail of every run.
|
||||
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/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
||||
merged into the skill repo's copy in Gitea by `push-skill`).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -104,10 +104,17 @@ Run it:
|
|||
python scripts/altium365_part_request.py --manifest part_requests.json \
|
||||
--base https://<workspace>.365.altium.com [--review-first | --no-submit]
|
||||
```
|
||||
`--no-submit` fills every form without saving (dry run); `--review-first` pauses after the first
|
||||
form so the operator can eyeball the mapping before the batch submits. The skill writes the
|
||||
`part_requests.json` manifest and commits each component's files to the operator's disk (device
|
||||
bridge) so the local `files` paths resolve.
|
||||
Default operation is **review mode**: the runner (`local_runner.py`) invokes the submitter with
|
||||
`--no-submit`, so it fills the form completely — fields, parameters, and all attachments — and
|
||||
**leaves it on screen for the operator to review and click Save**. This is the review-first gate
|
||||
and works even unattended (a console "press Enter" can't). Set `"auto_submit": true` in
|
||||
`runner_config.json` only to submit without review. (`--review-first` is a console-only variant
|
||||
that pauses for Enter.)
|
||||
|
||||
Build the manifest with `scripts/build_part_request_manifest.py` (it maps the typeid to the
|
||||
Workspace Component Type and pulls manufacturer/MPN/Description/parameters from the schlib
|
||||
`params.json`). The skill writes `part_requests.json` and commits it + each component's files to
|
||||
the operator's inbox (device bridge) so the local `files` paths resolve.
|
||||
|
||||
## Safety and auditing
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build (or append to) a `part_requests.json` manifest for the Altium 365 Part Request end task,
|
||||
from a component's schlib `params.json` plus its local file paths. The skill runs this at the end
|
||||
of a run, then commits the manifest + the component's `.SchLib`/`.PcbLib`/datasheet into the
|
||||
operator's Altium Runner inbox; the local runner picks it up and fills the Part Request.
|
||||
|
||||
Usage:
|
||||
python build_part_request_manifest.py --params <schlib params.json> --typeid CER \
|
||||
--file "C:/Altium Runner/inbox/JMK105BJ105KV-F.SchLib" \
|
||||
--file "C:/Altium Runner/inbox/JMK105BJ105KV-F.PcbLib" \
|
||||
--file "C:/Altium Runner/inbox/JMK105BJ105KV-F.pdf" \
|
||||
--out "C:/Altium Runner/inbox/part_requests.json"
|
||||
|
||||
`--component-type` overrides the typeid→type mapping; `--assignee` sets the librarian.
|
||||
"""
|
||||
import argparse, json, os
|
||||
|
||||
# Vecmocon typeid -> Altium 365 Part Request "Component Type" (options captured live from the
|
||||
# form's dropdown). Edit as your Workspace's component-type list evolves; --component-type wins.
|
||||
TYPEID_TO_COMPONENT_TYPE = {
|
||||
"CER": "Capacitors", "ELE": "Capacitors", "TAN": "Capacitors", "PLY": "Capacitors",
|
||||
"FLM": "Capacitors", "SFY": "Capacitors", "SUP": "Capacitors",
|
||||
"FIX": "Resistors", "TFR": "Resistors", "MFR": "Resistors", "CFR": "Resistors",
|
||||
"MOR": "Resistors", "WWR": "Resistors", "SHT": "Resistors", "ARR": "Resistors",
|
||||
"POT": "Resistors", "FSR": "Resistors", "NTC": "Resistors", "PTC": "Resistors",
|
||||
"PWR": "Inductors", "FBD": "Inductors", "CMC": "Inductors", "RFI": "Inductors",
|
||||
"XFM": "Inductors", "CTX": "Inductors", "CPL": "Inductors",
|
||||
"REC": "Diodes", "FRD": "Diodes", "SCH": "Diodes", "SIC": "Diodes", "ZEN": "Diodes",
|
||||
"TVS": "Diodes", "ESD": "Diodes", "SWI": "Diodes", "BRG": "Diodes", "LED": "LED",
|
||||
"BJT": "Transistors", "MOS": "Transistors", "SCM": "Transistors", "GAN": "Transistors",
|
||||
"IGBT": "Transistors", "JFET": "Transistors", "DIG": "Transistors",
|
||||
"MCU": "Processors", "LDO": "Power Supply", "DCD": "Power Supply", "PMU": "Power Supply",
|
||||
"BMS": "Integrated Circuits", "DRV": "Drivers", "AMP": "Amplifiers", "CMP": "Amplifiers",
|
||||
"VRF": "Data Converters", "ADC": "Data Converters", "DAC": "Data Converters",
|
||||
"ISO": "Optoelectronics", "XCV": "Interface", "AFE": "Integrated Circuits", "MEM": "Memory",
|
||||
"LOG": "Logic", "SEN": "Sensors", "IFC": "Interface", "CLK": "Crystals & Oscillators",
|
||||
"SVR": "Power Supply", "MTR": "Integrated Circuits",
|
||||
"FUS": "Fuses", "RSF": "Fuses", "VAR": "Miscellaneous", "GDT": "Miscellaneous", "CBK": "Switches",
|
||||
"DCM": "Power Supply", "IDC": "Power Supply", "INV": "Power Supply", "OBC": "Power Supply",
|
||||
"CHG": "Power Supply", "PSU": "Power Supply", "RCM": "Power Supply",
|
||||
"RLS": "Relays", "RLP": "Relays", "SSR": "Relays", "RLR": "Relays", "CTC": "Relays",
|
||||
"SWT": "Switches", "PBT": "Switches", "DSW": "Switches", "RSW": "Switches", "RSY": "Switches",
|
||||
"CWB": "Connectors", "CBB": "Connectors", "HDR": "Connectors", "FFC": "Connectors",
|
||||
"USB": "Connectors", "PWC": "Connectors", "TBK": "Connectors",
|
||||
"ANC": "Radio&RF", "ANP": "Radio&RF", "ANE": "Radio&RF", "SAW": "Radio&RF", "RFM": "Radio&RF",
|
||||
"XTL": "Crystals & Oscillators", "OSC": "Crystals & Oscillators", "MMO": "Crystals & Oscillators",
|
||||
"RSN": "Crystals & Oscillators",
|
||||
"CLI": "Batteries", "CLF": "Batteries", "CCO": "Batteries", "CNI": "Batteries",
|
||||
"BPK": "Batteries", "CHL": "Batteries",
|
||||
"BUZ": "Audio", "PBZ": "Audio", "SPK": "Audio", "IND": "Optoelectronics",
|
||||
"DSG": "Optoelectronics", "OLE": "Optoelectronics", "LCD": "Optoelectronics", "TFT": "Optoelectronics",
|
||||
"STE": "Sensors", "SCU": "Sensors", "SVO": "Sensors", "SHA": "Sensors", "SIM": "Sensors",
|
||||
"SPR": "Sensors",
|
||||
"FAN": "Mechanicals", "HSK": "Mechanicals", "TPD": "Mechanicals",
|
||||
}
|
||||
|
||||
|
||||
def build_entry(params, typeid, component_type, files, assignee):
|
||||
p = params.get("parameters", {})
|
||||
ctype = component_type or TYPEID_TO_COMPONENT_TYPE.get((typeid or "").upper(), "Miscellaneous")
|
||||
mpn = p.get("Manufacturer Part") or params.get("component") or ""
|
||||
return {
|
||||
"manufacturer": p.get("Manufacturer", ""),
|
||||
"mpn": mpn,
|
||||
"description": p.get("Description", ""),
|
||||
"component_type": ctype,
|
||||
"assignee": assignee or params.get("assignee", ""),
|
||||
"parameters": {k: v for k, v in p.items() if v not in ("", None)},
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--params", required=True, help="the schlib params.json for the component")
|
||||
ap.add_argument("--typeid", help="component typeid (auto-maps to an Altium Component Type)")
|
||||
ap.add_argument("--component-type", help="Altium Component Type (overrides the typeid map)")
|
||||
ap.add_argument("--assignee", default="", help="librarian to assign the request to (optional)")
|
||||
ap.add_argument("--file", action="append", default=[],
|
||||
help="local path to attach (repeatable): .SchLib, .PcbLib, datasheet")
|
||||
ap.add_argument("--out", required=True, help="part_requests.json (created or appended to)")
|
||||
a = ap.parse_args()
|
||||
|
||||
params = json.load(open(a.params, encoding="utf-8"))
|
||||
entry = build_entry(params, a.typeid, a.component_type, a.file, a.assignee)
|
||||
data = json.load(open(a.out, encoding="utf-8")) if os.path.exists(a.out) else {"requests": []}
|
||||
data.setdefault("requests", []).append(entry)
|
||||
json.dump(data, open(a.out, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
|
||||
print(f"wrote {a.out} (+1 request: {entry['mpn']} -> {entry['component_type']}, "
|
||||
f"{len(a.file)} file(s))")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -74,8 +74,12 @@ def process(cfg, manifest):
|
|||
"--out", manifest + ".result.json"]
|
||||
if cfg.get("chromedriver"):
|
||||
cmd += ["--chromedriver", cfg["chromedriver"]]
|
||||
if cfg.get("review_first"):
|
||||
cmd += ["--review-first"]
|
||||
# Default is REVIEW mode: fill the form completely (fields + parameters + attachments) and
|
||||
# leave it on screen WITHOUT saving, so the operator reviews and clicks Save themselves.
|
||||
# This is the review-first gate and it works even when the runner runs unattended (unlike a
|
||||
# console "press Enter"). Set "auto_submit": true in the config only to submit without review.
|
||||
if not cfg.get("auto_submit", False):
|
||||
cmd += ["--no-submit"]
|
||||
subprocess.run(cmd, check=False)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"base": "https://vecmocon-technologies-pvt-ltd.365.altium.com",
|
||||
"inbox": "D:/User Data/Desktop/altium-library-master/_part_request_inbox",
|
||||
"processed": "D:/User Data/Desktop/altium-library-master/_part_request_inbox/processed",
|
||||
"inbox": "C:/Altium Runner/inbox",
|
||||
"processed": "C:/Altium Runner/inbox/processed",
|
||||
"chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe",
|
||||
"user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data",
|
||||
"user_data_dir": "C:/altium-runner/chrome-profile",
|
||||
"debug_port": 9222,
|
||||
"chromedriver": null,
|
||||
"review_first": false,
|
||||
"auto_submit": false,
|
||||
"poll_seconds": 30
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue