From 594f315c0fccc7ddcf613a6cd135fb40b35164ba Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 16 Jul 2026 08:19:59 +0000 Subject: [PATCH] Drop Altium 365 Part Request scripts + reference (skill ends at Gitea push) (by admin) --- references/part_request_web.md | 129 ---------------- scripts/altium365_part_request.py | 205 ------------------------- scripts/build_part_request_manifest.py | 94 ------------ scripts/local_runner.py | 108 ------------- scripts/runner_config.example.json | 11 -- 5 files changed, 547 deletions(-) delete mode 100644 references/part_request_web.md delete mode 100644 scripts/altium365_part_request.py delete mode 100644 scripts/build_part_request_manifest.py delete mode 100644 scripts/local_runner.py delete mode 100644 scripts/runner_config.example.json diff --git a/references/part_request_web.md b/references/part_request_web.md deleted file mode 100644 index 06ae855..0000000 --- a/references/part_request_web.md +++ /dev/null @@ -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:\\...\\.PcbLib","C:\\...\\.pdf"], - "assignee":"","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 `/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://.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. diff --git a/scripts/altium365_part_request.py b/scripts/altium365_part_request.py deleted file mode 100644 index c4dd574..0000000 --- a/scripts/altium365_part_request.py +++ /dev/null @@ -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 -`/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\\\\.PcbLib","C:\\\\lib\\\\.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://.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() diff --git a/scripts/build_part_request_manifest.py b/scripts/build_part_request_manifest.py deleted file mode 100644 index b7bc57c..0000000 --- a/scripts/build_part_request_manifest.py +++ /dev/null @@ -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 --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() diff --git a/scripts/local_runner.py b/scripts/local_runner.py deleted file mode 100644 index fdb8553..0000000 --- a/scripts/local_runner.py +++ /dev/null @@ -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() diff --git a/scripts/runner_config.example.json b/scripts/runner_config.example.json deleted file mode 100644 index 5bf9a01..0000000 --- a/scripts/runner_config.example.json +++ /dev/null @@ -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 -}