Drop Altium 365 Part Request scripts + reference (skill ends at Gitea push) (by admin)
parent
6083e4ee78
commit
594f315c0f
|
|
@ -1,129 +0,0 @@
|
||||||
# Submitting components as Altium 365 Part Requests (web browser)
|
|
||||||
|
|
||||||
When the central library is a managed **Altium 365 Workspace** and the operator is **not an
|
|
||||||
admin**, the skill's end task can be to submit each finished component as a **Part Request**
|
|
||||||
through the Workspace's web UI, driven by browser automation (Claude-in-Chrome). A librarian
|
|
||||||
then approves each request into the managed library. This runs in the operator's **own Chrome,
|
|
||||||
using their existing Altium 365 login**, so it needs **no API token and no admin rights** — it
|
|
||||||
just does what the engineer would do by hand, for every component in the run.
|
|
||||||
|
|
||||||
This is the browser alternative to the headless API push (`altium365_push.py`, if a token is
|
|
||||||
ever available) and to the Gitea push. Use whichever matches how the org consumes the library.
|
|
||||||
|
|
||||||
## Prerequisites (each run)
|
|
||||||
|
|
||||||
- **Chrome open** with the Claude-in-Chrome extension enabled, and **site permission granted**
|
|
||||||
for the Workspace domain (e.g. `vecmocon.altium365.com`).
|
|
||||||
- **Signed in** to the Altium 365 Workspace as any member with rights to create Part Requests.
|
|
||||||
- **The component files on the LOCAL machine** — the browser uploads attachments from local
|
|
||||||
disk, so each component's `.SchLib` (with parameters written), `.PcbLib`, and datasheet must
|
|
||||||
exist on the operator's computer. If the skill produced them in the cloud, commit them to the
|
|
||||||
device first (device bridge) into a known folder, and put those local paths in the manifest.
|
|
||||||
- **The manifest** of components to submit this run (below).
|
|
||||||
- **Decisions the operator confirms once**: the default **Assign to** (the librarian/group), the
|
|
||||||
**Component Type** mapping for each typeid, and an optional **Required By** date.
|
|
||||||
|
|
||||||
## Per-component field mapping (the Part Request form)
|
|
||||||
|
|
||||||
| Form field | Value (from the skill's per-part data) |
|
|
||||||
|---|---|
|
|
||||||
| Manufacturer | `Manufacturer` param (e.g. `Taiyo Yuden`) |
|
|
||||||
| Manufacturer Part Numbers | `Manufacturer Part` param (the MPN) |
|
|
||||||
| Description | `Description` param (SOP format, e.g. `CHIP_CAP_1uF_6.3v_±10%_0402_x5r`) |
|
|
||||||
| Component Type | the typeid mapped to the Workspace's matching component type (confirm once per typeid) |
|
|
||||||
| State | leave `Opened: New` |
|
|
||||||
| Required By Date | optional org default |
|
|
||||||
| Assign to | the configured librarian / group |
|
|
||||||
| Parameters → Add | every parameter from the full set (name + value) |
|
|
||||||
| Attachments | the `.SchLib`, `.PcbLib`, and datasheet files |
|
|
||||||
| Parts List → Add | optional: add the MPN as a part choice so the librarian's mapping is pre-seeded |
|
|
||||||
|
|
||||||
## The manifest
|
|
||||||
|
|
||||||
The skill writes one `part_requests.json` per run listing every component it processed, so the
|
|
||||||
browser step can loop without re-deriving anything:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"requests":[
|
|
||||||
{"manufacturer":"Taiyo Yuden","mpn":"JMK105BJ105KV-F",
|
|
||||||
"description":"CHIP_CAP_1uF_6.3v_±10%_0402_x5r","component_type":"Capacitor",
|
|
||||||
"parameters":{"Value":"1u","Voltage(V)":"6.3","Tolerance":"±10%","...":"..."},
|
|
||||||
"files":["C:\\...\\JMK105BJ105KV-F.SchLib","C:\\...\\<footprint>.PcbLib","C:\\...\\<mpn>.pdf"],
|
|
||||||
"assignee":"<librarian>","required_by":""}
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Build it from each component's `params.json` plus the local file paths (after committing files
|
|
||||||
to the device).
|
|
||||||
|
|
||||||
## Browser procedure (looped per component)
|
|
||||||
|
|
||||||
Start the browser session with `tabs_context_mcp`, then for each entry in the manifest:
|
|
||||||
|
|
||||||
1. Navigate to **Library → Part Requests → new request**.
|
|
||||||
2. Fill **Manufacturer**, **Manufacturer Part Numbers**, **Description**.
|
|
||||||
3. Choose **Component Type**; set **Assign to**; optional **Required By Date**. Leave State as
|
|
||||||
`Opened: New`.
|
|
||||||
4. **Parameters → Add**: add each parameter name + value.
|
|
||||||
5. **Attachments** (Choose file / drop): upload the `.SchLib`, `.PcbLib`, and datasheet.
|
|
||||||
6. Review, then **Save**. Record the auto-assigned **Request Id**.
|
|
||||||
7. Move to the next entry.
|
|
||||||
|
|
||||||
## Standalone Selenium path (token-free at scale) — `scripts/altium365_part_request.py`
|
|
||||||
|
|
||||||
Live browser-driving costs Claude tokens per component. For a whole library, use the Selenium
|
|
||||||
script instead: it's authored once, then runs on the operator's machine over any number of
|
|
||||||
components with **zero Claude tokens per part**. It attaches to the operator's already-signed-in
|
|
||||||
Chrome (remote-debugging port), so it reuses the Altium 365 session — no credentials handled.
|
|
||||||
|
|
||||||
Captured selectors (live form at `<workspace>/partrequestsmanagement/Tasks/Add`; Altium "afs"
|
|
||||||
design system + Selectize.js dropdowns):
|
|
||||||
|
|
||||||
- Manufacturer → `input#Manufacturer`
|
|
||||||
- Manufacturer Part Numbers → `textarea#ManufacturerPartNumbers`
|
|
||||||
- Description → `textarea#Description`
|
|
||||||
- Component Type / Assign to → Selectize: click the `.selectize-input` after the label, then the
|
|
||||||
`.selectize-dropdown-content .option` whose text matches.
|
|
||||||
- Parameters → afs-table under the "Parameters" label; each "Add" (`a.afs-link`) click adds a
|
|
||||||
`div.afs-table__tr` with two `input.afs-input__control` (name, value).
|
|
||||||
- Attachments → `input#fileupload` (Selenium `send_keys` the local file path(s)).
|
|
||||||
- Save → `button.afs-btn_primary` (text "Save").
|
|
||||||
|
|
||||||
Component Type options (must match one exactly): Test Points, Clock&Timing, Memory, Wireless,
|
|
||||||
Transformers, Mechanicals, Capacitors, Transistors, Drivers, Optoelectronics, Power Supply,
|
|
||||||
Audio, Fuses, Switches, Integrated Circuits, Logic, Diodes, Interface, Miscellaneous, Radio&RF,
|
|
||||||
Sensors, Processors, Mechanical, Inductors, LED, Amplifiers, Resistors, Data Converters, Relays,
|
|
||||||
Batteries, Connectors, Crystals & Oscillators. (Map each typeid to one of these when building the
|
|
||||||
manifest — e.g. `CER → Capacitors`.)
|
|
||||||
|
|
||||||
Run it:
|
|
||||||
```bash
|
|
||||||
# 1) close Chrome, relaunch on the operator's own profile with a debug port:
|
|
||||||
# chrome.exe --remote-debugging-port=9222 --user-data-dir="%LOCALAPPDATA%\Google\Chrome\User Data"
|
|
||||||
# 2) sign into the Workspace in that Chrome, then:
|
|
||||||
python scripts/altium365_part_request.py --manifest part_requests.json \
|
|
||||||
--base https://<workspace>.365.altium.com [--review-first | --no-submit]
|
|
||||||
```
|
|
||||||
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
|
|
||||||
|
|
||||||
- On the **first component of a run**, fill everything and **stop at Save** for the operator to
|
|
||||||
eyeball the mapping. Once they confirm it looks right, Save it and loop the rest unattended.
|
|
||||||
- **Log every submitted Request Id** (and any component that failed) so the run is auditable and
|
|
||||||
re-runnable — never silently skip a component.
|
|
||||||
- Browser automation follows the live UI. If a field, dropdown option, or a popup doesn't match
|
|
||||||
what's expected, **pause and ask** rather than guessing — a wrong Component Type or a
|
|
||||||
half-filled request is worse than one clarifying question.
|
|
||||||
- Don't trigger native file-dialog blocking: use the extension's file-upload path for
|
|
||||||
attachments, not an OS dialog.
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Submit Altium 365 **Part Requests** with Selenium, looping over a manifest — the skill's
|
|
||||||
end task when the central library is a managed Altium 365 Workspace and the operator is not an
|
|
||||||
admin.
|
|
||||||
|
|
||||||
It runs on the OPERATOR'S machine and **attaches to their already-signed-in Chrome** via the
|
|
||||||
remote-debugging port, so it reuses the existing Altium 365 session — no credentials are ever
|
|
||||||
handled here. It then fills and submits the "Add new request" web form for each component.
|
|
||||||
|
|
||||||
Selectors were captured from the live form at
|
|
||||||
`<workspace>/partrequestsmanagement/Tasks/Add` (Altium's "afs" design system + Selectize.js
|
|
||||||
dropdowns):
|
|
||||||
- Manufacturer : input#Manufacturer
|
|
||||||
- Manufacturer Part Numbers: textarea#ManufacturerPartNumbers
|
|
||||||
- Description : textarea#Description
|
|
||||||
- Component Type / Assign to: Selectize dropdowns (click control after the label, click the
|
|
||||||
option div by text)
|
|
||||||
- Parameters : an afs-table under the "Parameters" label; each "Add" click adds a
|
|
||||||
row of two inputs (name, value)
|
|
||||||
- Attachments : input#fileupload (Selenium send_keys the local file path)
|
|
||||||
- Save : button.afs-btn_primary with text "Save"
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------------------
|
|
||||||
SETUP (once, on the operator's machine)
|
|
||||||
1. pip install selenium (Selenium 4+ auto-manages chromedriver; or pass --chromedriver)
|
|
||||||
2. Fully close Chrome, then start it with a debugging port on the operator's own profile so
|
|
||||||
the Altium 365 login is reused, e.g. on Windows:
|
|
||||||
chrome.exe --remote-debugging-port=9222 --user-data-dir="%LOCALAPPDATA%\\Google\\Chrome\\User Data"
|
|
||||||
Sign in to the Workspace in that Chrome if not already.
|
|
||||||
3. Run:
|
|
||||||
python altium365_part_request.py --manifest part_requests.json \
|
|
||||||
--base https://vecmocon-technologies-pvt-ltd.365.altium.com
|
|
||||||
Add --no-submit to fill every form but NOT click Save (dry run), or --review-first to
|
|
||||||
pause after filling the first request so you can eyeball it before the batch proceeds.
|
|
||||||
|
|
||||||
MANIFEST (part_requests.json) — one entry per component:
|
|
||||||
{"requests":[
|
|
||||||
{"manufacturer":"Taiyo Yuden","mpn":"JMK105BJ105KV-F",
|
|
||||||
"description":"CHIP_CAP_1uF_6.3v_±10%_0402_x5r","component_type":"Capacitors",
|
|
||||||
"assignee":"", // optional: exact name as shown in the Assign-to list
|
|
||||||
"parameters":{"Value":"1u","Voltage(V)":"6.3","Tolerance":"±10%"},
|
|
||||||
"files":["C:\\\\lib\\\\JMK105BJ105KV-F.SchLib","C:\\\\lib\\\\<fp>.PcbLib","C:\\\\lib\\\\<mpn>.pdf"]}
|
|
||||||
]}
|
|
||||||
|
|
||||||
Component Type must be one of the Workspace's list (captured live):
|
|
||||||
Test Points, Clock&Timing, Memory, Wireless, Transformers, Mechanicals, Capacitors,
|
|
||||||
Transistors, Drivers, Optoelectronics, Power Supply, Audio, Fuses, Switches,
|
|
||||||
Integrated Circuits, Logic, Diodes, Interface, Miscellaneous, Radio&RF, Sensors, Processors,
|
|
||||||
Mechanical, Inductors, LED, Amplifiers, Resistors, Data Converters, Relays, Batteries,
|
|
||||||
Connectors, Crystals & Oscillators
|
|
||||||
"""
|
|
||||||
import argparse, json, os, sys, time
|
|
||||||
from selenium import webdriver
|
|
||||||
from selenium.webdriver.chrome.options import Options
|
|
||||||
from selenium.webdriver.chrome.service import Service
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
|
||||||
from selenium.common.exceptions import TimeoutException
|
|
||||||
|
|
||||||
|
|
||||||
def make_driver(debugger_address, chromedriver):
|
|
||||||
opts = Options()
|
|
||||||
opts.add_experimental_option("debuggerAddress", debugger_address)
|
|
||||||
return webdriver.Chrome(service=Service(chromedriver), options=opts) if chromedriver \
|
|
||||||
else webdriver.Chrome(options=opts)
|
|
||||||
|
|
||||||
|
|
||||||
def _wait(driver, cond, t=25):
|
|
||||||
return WebDriverWait(driver, t).until(cond)
|
|
||||||
|
|
||||||
|
|
||||||
def _set_text(driver, by, sel, value):
|
|
||||||
el = _wait(driver, EC.presence_of_element_located((by, sel)))
|
|
||||||
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
|
|
||||||
el.clear()
|
|
||||||
el.send_keys(value or "")
|
|
||||||
|
|
||||||
|
|
||||||
def _select_dropdown(driver, label, value):
|
|
||||||
"""Selectize: click the control after `label`, then the visible option whose text == value."""
|
|
||||||
control = _wait(driver, EC.element_to_be_clickable((By.XPATH,
|
|
||||||
f"//*[normalize-space(text())='{label}']/following::div[contains(@class,'selectize-input')][1]")))
|
|
||||||
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", control)
|
|
||||||
control.click()
|
|
||||||
opt = _wait(driver, EC.element_to_be_clickable((By.XPATH,
|
|
||||||
"//div[contains(@class,'selectize-dropdown') and not(contains(@style,'display: none'))]"
|
|
||||||
f"//div[contains(@class,'option') and normalize-space()=\"{value}\"]")))
|
|
||||||
opt.click()
|
|
||||||
|
|
||||||
|
|
||||||
def _add_parameters(driver, params):
|
|
||||||
"""Click the Parameters 'Add' link once per parameter, then fill each row's (name,value)."""
|
|
||||||
if not params:
|
|
||||||
return
|
|
||||||
add_link_xpath = ("//*[normalize-space(text())='Parameters']/descendant-or-self::*"
|
|
||||||
"/a[contains(@class,'afs-link')] | "
|
|
||||||
"//*[normalize-space(text())='Parameters']/following::a[contains(@class,'afs-link')][1]")
|
|
||||||
for _ in params:
|
|
||||||
link = _wait(driver, EC.element_to_be_clickable((By.XPATH, add_link_xpath)))
|
|
||||||
driver.execute_script("arguments[0].click();", link)
|
|
||||||
time.sleep(0.25)
|
|
||||||
# the Parameters afs-table tbody that sits under the "Parameters" label
|
|
||||||
tbody = _wait(driver, EC.presence_of_element_located((By.XPATH,
|
|
||||||
"//*[normalize-space(text())='Parameters']/following::div[contains(@class,'afs-table__tbody')][1]")))
|
|
||||||
rows = tbody.find_elements(By.XPATH, ".//div[contains(@class,'afs-table__tr')]")
|
|
||||||
for row, (name, val) in zip(rows, params.items()):
|
|
||||||
cells = row.find_elements(By.XPATH, ".//input[contains(@class,'afs-input__control')]")
|
|
||||||
if len(cells) >= 2:
|
|
||||||
cells[0].clear(); cells[0].send_keys(str(name))
|
|
||||||
cells[1].clear(); cells[1].send_keys(str(val))
|
|
||||||
|
|
||||||
|
|
||||||
def _attach_files(driver, files):
|
|
||||||
present = [f for f in (files or []) if f and os.path.exists(f)]
|
|
||||||
missing = [f for f in (files or []) if f and not os.path.exists(f)]
|
|
||||||
for f in present: # one at a time so multiple files accumulate
|
|
||||||
inp = _wait(driver, EC.presence_of_element_located((By.ID, "fileupload")))
|
|
||||||
inp.send_keys(os.path.abspath(f))
|
|
||||||
time.sleep(0.8)
|
|
||||||
return present, missing
|
|
||||||
|
|
||||||
|
|
||||||
def submit_request(driver, base, req, no_submit=False):
|
|
||||||
url = f"{base}/partrequestsmanagement/Tasks/Add"
|
|
||||||
err = None
|
|
||||||
for attempt in range(3): # retry: first nav can be slow/cold
|
|
||||||
driver.get(url)
|
|
||||||
try:
|
|
||||||
_wait(driver, EC.presence_of_element_located((By.ID, "Manufacturer")), t=20)
|
|
||||||
err = None
|
|
||||||
break
|
|
||||||
except TimeoutException as e:
|
|
||||||
err = e
|
|
||||||
time.sleep(2)
|
|
||||||
if err is not None:
|
|
||||||
cur = driver.current_url
|
|
||||||
if any(k in cur.lower() for k in ("auth.altium.com", "signin", "login", "oauth")):
|
|
||||||
raise RuntimeError(
|
|
||||||
"NOT SIGNED IN — this Chrome (the one on the debug port) is on the Altium login "
|
|
||||||
"page, so the form never loaded. Open that same Chrome profile, sign into the "
|
|
||||||
"Workspace (keep me signed in), leave it open, and re-run.")
|
|
||||||
raise RuntimeError(f"The 'Add new request' form did not load (no #Manufacturer field). "
|
|
||||||
f"Current page URL: {cur}")
|
|
||||||
_set_text(driver, By.ID, "Manufacturer", req.get("manufacturer", ""))
|
|
||||||
_set_text(driver, By.ID, "ManufacturerPartNumbers", req.get("mpn", ""))
|
|
||||||
_set_text(driver, By.ID, "Description", req.get("description", ""))
|
|
||||||
if req.get("component_type"):
|
|
||||||
_select_dropdown(driver, "Component Type", req["component_type"])
|
|
||||||
if req.get("assignee"):
|
|
||||||
_select_dropdown(driver, "Assign to", req["assignee"])
|
|
||||||
_add_parameters(driver, req.get("parameters") or {})
|
|
||||||
attached, missing = _attach_files(driver, req.get("files"))
|
|
||||||
if no_submit:
|
|
||||||
return {"mpn": req.get("mpn"), "status": "filled_not_submitted",
|
|
||||||
"attached": attached, "missing_files": missing}
|
|
||||||
save = _wait(driver, EC.element_to_be_clickable((By.XPATH,
|
|
||||||
"//button[contains(@class,'afs-btn_primary') and normalize-space()='Save']")))
|
|
||||||
driver.execute_script("arguments[0].click();", save)
|
|
||||||
time.sleep(2.5) # let the SPA post and return to the list
|
|
||||||
return {"mpn": req.get("mpn"), "status": "submitted",
|
|
||||||
"attached": attached, "missing_files": missing}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser(description="Submit Altium 365 Part Requests via Selenium.")
|
|
||||||
ap.add_argument("--manifest", required=True, help="part_requests.json")
|
|
||||||
ap.add_argument("--base", required=True, help="Workspace base URL, e.g. https://<ws>.365.altium.com")
|
|
||||||
ap.add_argument("--debugger-address", default="127.0.0.1:9222")
|
|
||||||
ap.add_argument("--chromedriver", default=None, help="path to chromedriver (optional in Selenium 4+)")
|
|
||||||
ap.add_argument("--no-submit", action="store_true", help="fill every form but do not click Save")
|
|
||||||
ap.add_argument("--review-first", action="store_true",
|
|
||||||
help="after filling the first request, pause for Enter before submitting the rest")
|
|
||||||
ap.add_argument("--out", default="part_requests_result.json")
|
|
||||||
a = ap.parse_args()
|
|
||||||
|
|
||||||
data = json.load(open(a.manifest, encoding="utf-8"))
|
|
||||||
requests = data.get("requests", [])
|
|
||||||
driver = make_driver(a.debugger_address, a.chromedriver)
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
for i, req in enumerate(requests):
|
|
||||||
first = (i == 0)
|
|
||||||
no_submit = a.no_submit or (a.review_first and first)
|
|
||||||
try:
|
|
||||||
r = submit_request(driver, a.base.rstrip("/"), req, no_submit=no_submit)
|
|
||||||
except Exception as e:
|
|
||||||
r = {"mpn": req.get("mpn"), "status": "error", "error": str(e).splitlines()[0][:400]}
|
|
||||||
results.append(r)
|
|
||||||
print(f"[{i+1}/{len(requests)}] {req.get('mpn')}: {r['status']}"
|
|
||||||
+ (f" -> {r['error']}" if r.get("error") else "")
|
|
||||||
+ (f" MISSING {r['missing_files']}" if r.get("missing_files") else ""))
|
|
||||||
if a.review_first and first and not a.no_submit:
|
|
||||||
input("Review the first request in the browser, then press Enter to submit the rest...")
|
|
||||||
# re-submit the first for real now that it's been reviewed
|
|
||||||
results[-1] = submit_request(driver, a.base.rstrip("/"), req, no_submit=False)
|
|
||||||
finally:
|
|
||||||
json.dump({"results": results}, open(a.out, "w"), indent=2, ensure_ascii=False)
|
|
||||||
print(f"\nwrote {a.out} ({sum(1 for r in results if r['status']=='submitted')} submitted, "
|
|
||||||
f"{sum(1 for r in results if r['status']=='error')} errors)")
|
|
||||||
# NOTE: attaching to your Chrome — the script does not close your browser.
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
#!/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()
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""LOCAL RUNNER — runs on the operator's own machine (Windows), set up ONCE, to make Altium 365
|
|
||||||
Part Request submission the automatic tail of a skill run.
|
|
||||||
|
|
||||||
Why this exists: the library-manager skill runs in the cloud, but the Part Request web form must
|
|
||||||
be driven from the operator's signed-in Chrome on their desktop — the cloud can't reach it. So at
|
|
||||||
the end of a run the skill drops the finished files + a `part_requests.json` manifest into an
|
|
||||||
**inbox folder on this machine** (via the device bridge). This runner watches that inbox and, for
|
|
||||||
each manifest: makes sure Chrome is running with a debugging port on the operator's profile
|
|
||||||
(launching it if needed — the persisted login is reused), runs `altium365_part_request.py`, then
|
|
||||||
moves the manifest to a `processed/` folder.
|
|
||||||
|
|
||||||
Set it up once (Task Scheduler at logon, or a startup shortcut) and every future skill run
|
|
||||||
auto-submits its Part Requests with no further action — as long as this machine is on and the
|
|
||||||
Chrome profile has been signed into the Workspace at least once.
|
|
||||||
|
|
||||||
Config: a `runner_config.json` next to this file, e.g.
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe",
|
|
||||||
"user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data",
|
|
||||||
"debug_port": 9222,
|
|
||||||
"chromedriver": null,
|
|
||||||
"review_first": false,
|
|
||||||
"poll_seconds": 30
|
|
||||||
}
|
|
||||||
|
|
||||||
Run:
|
|
||||||
python local_runner.py # watch the inbox forever (use for Task Scheduler / startup)
|
|
||||||
python local_runner.py --once # process whatever is in the inbox now, then exit
|
|
||||||
"""
|
|
||||||
import glob, json, os, shutil, socket, subprocess, sys, time
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def load_cfg():
|
|
||||||
with open(os.path.join(HERE, "runner_config.json"), encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
def _port_open(port):
|
|
||||||
s = socket.socket(); s.settimeout(1)
|
|
||||||
try:
|
|
||||||
s.connect(("127.0.0.1", int(port))); return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_chrome(cfg):
|
|
||||||
"""Make sure a Chrome with the debug port is up (reusing the operator's logged-in profile)."""
|
|
||||||
if _port_open(cfg["debug_port"]):
|
|
||||||
return
|
|
||||||
udd = os.path.expandvars(cfg["user_data_dir"])
|
|
||||||
subprocess.Popen([cfg["chrome"], f"--remote-debugging-port={cfg['debug_port']}",
|
|
||||||
f"--user-data-dir={udd}"])
|
|
||||||
for _ in range(40):
|
|
||||||
if _port_open(cfg["debug_port"]):
|
|
||||||
time.sleep(2) # give the profile a moment to restore the session
|
|
||||||
return
|
|
||||||
time.sleep(1)
|
|
||||||
raise RuntimeError("Chrome did not expose a debug port — check 'chrome' path and 'debug_port'.")
|
|
||||||
|
|
||||||
|
|
||||||
def process(cfg, manifest):
|
|
||||||
ensure_chrome(cfg)
|
|
||||||
cmd = [sys.executable, os.path.join(HERE, "altium365_part_request.py"),
|
|
||||||
"--manifest", manifest, "--base", cfg["base"],
|
|
||||||
"--debugger-address", f"127.0.0.1:{cfg['debug_port']}",
|
|
||||||
"--out", manifest + ".result.json"]
|
|
||||||
if cfg.get("chromedriver"):
|
|
||||||
cmd += ["--chromedriver", cfg["chromedriver"]]
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
cfg = load_cfg()
|
|
||||||
os.makedirs(cfg["inbox"], exist_ok=True)
|
|
||||||
os.makedirs(cfg["processed"], exist_ok=True)
|
|
||||||
once = "--once" in sys.argv
|
|
||||||
print(f"watching {cfg['inbox']} (base {cfg['base']})")
|
|
||||||
while True:
|
|
||||||
for m in sorted(glob.glob(os.path.join(cfg["inbox"], "*.json"))):
|
|
||||||
if m.endswith(".result.json"):
|
|
||||||
continue
|
|
||||||
print("processing", m)
|
|
||||||
try:
|
|
||||||
process(cfg, m)
|
|
||||||
except Exception as e:
|
|
||||||
print(" error:", e)
|
|
||||||
shutil.move(m, os.path.join(cfg["processed"], os.path.basename(m)))
|
|
||||||
if once:
|
|
||||||
break
|
|
||||||
time.sleep(int(cfg.get("poll_seconds", 30)))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"base": "https://vecmocon-technologies-pvt-ltd.365.altium.com",
|
|
||||||
"inbox": "C:/Altium Runner/inbox",
|
|
||||||
"processed": "C:/Altium Runner/inbox/processed",
|
|
||||||
"chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe",
|
|
||||||
"user_data_dir": "C:/altium-runner/chrome-profile",
|
|
||||||
"debug_port": 9222,
|
|
||||||
"chromedriver": null,
|
|
||||||
"auto_submit": false,
|
|
||||||
"poll_seconds": 30
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue