diff --git a/ACCEPT_JOBS_README.md b/ACCEPT_JOBS_README.md new file mode 100644 index 0000000..f58aa69 --- /dev/null +++ b/ACCEPT_JOBS_README.md @@ -0,0 +1,112 @@ +# Bulk Import Job Acceptance Automation + +This feature automates the acceptance of bulk import jobs in PartDB. + +## What It Does + +The automation will: +1. Navigate to your import job page (or you can navigate there manually) +2. Find all selectable "Update Part" buttons (only `btn btn-primary` without the `disabled` class) +3. For each button: + - Click the button and wait for the page to load (stays on same page) + - Click "Save" and wait for the page to load + - Click "Save" again and wait for the page to load + - Click "Complete" to finish the job +4. Repeat until no more enabled "Update Part" buttons are found + +## How to Use + +### Option 1: From the UI (Recommended) + +1. Run the main application: `python main.py` +2. On the home page, click the **"Accept Import Jobs"** button in the Tools section +3. A browser window will open +4. When prompted, navigate to the import job page where the "Update part" buttons are +5. Press Enter in the console to start the automation +6. Watch as the automation processes each job +7. When complete, press Enter to close the browser + +### Option 2: Standalone Script + +1. Open PowerShell/Terminal +2. Run: `python workflows\accept_import_jobs.py` +3. Follow the same steps as above + +### Option 3: With Direct URL + +If you know the exact URL of the import job page, you can modify the script: + +```python +from workflows.accept_import_jobs import run_accept_import_jobs + +# Provide the direct URL +run_accept_import_jobs("https://partdb.neutronservices.duckdns.org/en/import/jobs/123") +``` + +## Configuration + +In `config.py`, you can adjust: + +```python +# Maximum number of jobs to process in one run (prevents infinite loops) +ACCEPT_JOBS_MAX_ITERATIONS = 100 + +# Delay between job attempts (seconds) +ACCEPT_JOBS_RETRY_DELAY = 1.0 + +# Whether to run browser in headless mode +HEADLESS_CONTROLLER = False # Set to True to hide the browser window +``` + +## Button Detection + +The automation specifically looks for "Update Part" buttons that: +- Have the class `btn btn-primary` (indicating a clickable button) +- Do **NOT** have the `disabled` class (which would make them unclickable) + +This ensures only valid, actionable import jobs are processed and disabled buttons are skipped. + +Button texts detected: +- "Update Part" +- "Update part" + +And will click Save/Complete buttons with these texts: +- "Save" +- "Save changes" +- "Complete" + +**Important:** The automation filters out any buttons with `class="btn btn-primary disabled"` to avoid clicking non-actionable buttons. + +## Troubleshooting + +### No buttons found +- Make sure you're on the correct page with import jobs +- Check that there are "Update Part" buttons with class `btn btn-primary` (without `disabled`) +- The buttons must be visible, enabled, and not have the `disabled` class +- Only jobs that are ready to process will have enabled buttons + +### Automation stops early +- Check the console output for error messages +- Some jobs might have different button text or layout +- You can adjust the XPath selectors in `provider/selenium_flow.py` if needed + +### Browser closes immediately +- Make sure you press Enter only when you're on the correct page +- Check that you're logged in to PartDB + +## Statistics + +After completion, you'll see: +- Number of jobs successfully processed +- Number of jobs that failed +- Total time taken + +## Technical Details + +The automation uses: +- **Selenium WebDriver** for browser automation +- **Firefox** as the default browser (with Chrome fallback) +- **Robust element detection** that handles stale elements and page reloads +- **Automatic retry logic** for clicking buttons + +The main function is `accept_bulk_import_jobs()` in `provider/selenium_flow.py`. diff --git a/Import CSVs/voltage_reference.csv b/Import CSVs/voltage_reference.csv new file mode 100644 index 0000000..ffd0ec5 --- /dev/null +++ b/Import CSVs/voltage_reference.csv @@ -0,0 +1,7 @@ +Datasheet,Image,DK Part #,Mfr Part #,Mfr,Supplier,Description,Stock,Price,@ qty,Min Qty, Package,Series,Product Status,Reference Type,Output Type,Voltage - Output (Min/Fixed),Current - Output,Tolerance,Temperature Coefficient,Noise - 0.1Hz to 10Hz,Noise - 10Hz to 10kHz,Voltage - Input,Current - Supply,Operating Temperature,Mounting Type,Package / Case,Supplier Device Package,URL +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"296-26324-2-ND,296-26324-1-ND,296-26324-6-ND",REF3033AIDBZR,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"24,432",1.75,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,3.3V,25 mA,±0.2%,75ppm/°C,36µVp-p,105µVrms,3.35V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3033AIDBZR/1573916 +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"296-26321-2-ND,296-26321-1-ND,296-26321-6-ND",REF3020AIDBZR,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"19,223",1.75,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,2.048V,25 mA,±0.2%,75ppm/°C,23µVp-p,65µVrms,2.098V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3020AIDBZR/1573908 +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"296-26323-2-ND,296-26323-1-ND,296-26323-6-ND",REF3030AIDBZR,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"11,327",1.75,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,3V,25 mA,±0.2%,75ppm/°C,33µVp-p,94µVrms,3.05V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3030AIDBZR/1573913 +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"296-26322-2-ND,296-26322-1-ND,296-26322-6-ND",REF3025AIDBZR,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"11,179",1.75,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,2.5V,25 mA,±0.2%,75ppm/°C,28µVp-p,80µVrms,2.55V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3025AIDBZR/1573911 +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"296-32213-2-ND,296-32213-1-ND,296-32213-6-ND",REF3012AIDBZR,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"3,143",1.75,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,1.25V,25 mA,±0.2%,75ppm/°C,14µVp-p,42µVrms,1.8V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3012AIDBZR/1573905 +https://www.ti.com/lit/ds/symlink/ref30.pdf?ts=1743588330976&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FREF30%252Fpart-details%252FREF3033AIDBZT%253FkeyMatch%253DREF3033AIDBZT%2526tisearch%253Duniversal_search%2526usecase%253DOPN,//mm.digikey.com/Volume0/opasdata/d220001/medias/images/3969/296%7E4203227%7EDBZ%7E3.JPG,"REF3040AIDBZTTR-ND,REF3040AIDBZTCT-ND,REF3040AIDBZTDKR-ND",REF3040AIDBZT,Texas Instruments,Texas Instruments,IC VREF SERIES 0.2% SOT23-3,"7,615",2.06,0,1,"Tape & Reel (TR),Cut Tape (CT),Digi-Reel®",-,Active,Series,Fixed,4.096V,25 mA,±0.2%,75ppm/°C,45µVp-p,128µVrms,4.146V ~ 5.5V,50µA,-40°C ~ 125°C (TA),Surface Mount,"TO-236-3, SC-59, SOT-23-3",SOT-23-3,https://www.digikey.com.au/en/products/detail/texas-instruments/REF3040AIDBZT/459294 diff --git a/config.py b/config.py index 6f07c5c..9f29682 100644 --- a/config.py +++ b/config.py @@ -5,7 +5,7 @@ PARTDB_TOKEN = "tcp_564c6518a8476c25c68778e640c1bf40eecdec9f67be580bbd6504e9b6e UI_LANG_PATH = "/en" # Modes: "bulk" or "scan" -MODE = "scan" +MODE = "bulk" # Scanner COM_PORT = "COM7" @@ -30,4 +30,19 @@ ENV_USER = "Nick" ENV_PASSWORD = "O@IyECa^XND7BvPpRX9XRKBhv%XVwCV4" # UI defaults -WINDOW_GEOM = "860x560" \ No newline at end of file +WINDOW_GEOM = "860x560" + +# Bulk import job acceptance +ACCEPT_JOBS_MAX_ITERATIONS = 100 # Maximum number of jobs to process in one run +ACCEPT_JOBS_RETRY_DELAY = 1.0 # Delay between job attempts (seconds) + +# Bulk add workflow settings +ENABLE_RESISTORS_0805 = False +ENABLE_RESISTORS_0603 = False +ENABLE_CAPS_0805 = False +ADD_1206_FOR_LOW_V_CAPS = False +LOW_V_CAP_THRESHOLD_V = 10.0 +UPSIZED_1206_TARGET_V = "25V" +DEFAULT_CAP_MANUFACTURER = "Samsung" +MAX_TO_CREATE = None # None for all, or a number to limit +SKIP_IF_EXISTS = True diff --git a/parsers/values.py b/parsers/values.py index d99dee5..8ca1228 100644 --- a/parsers/values.py +++ b/parsers/values.py @@ -1,7 +1,7 @@ import re, math from typing import Optional, List -RE_RES_SIMPLE = re.compile(r"(?i)^\s*(\d+(?:\.\d+)?)\s*(ohm|ohms|r|k|m|kohm|mohm|kΩ|mΩ|kOhm|MOhm)?\s*$") +RE_RES_SIMPLE = re.compile(r"(?i)^\s*(\d+(?:\.\d+)?)\s*(ohm|ohms|r|k|m|kohm|kohms|mohm|mohms|kΩ|mΩ|kOhm|kOhms|MOhm|MOhms)?\s*$") RE_RES_LETTER = re.compile(r"(?i)^\s*(\d+)([rkm])(\d+)?\s*$") RE_CAP_SIMPLE = re.compile(r"(?i)^(\d+(?:\.\d+)?)(p|n|u|m|pf|nf|uf|mf|f)?$") RE_CAP_LETTER = re.compile(r"(?i)^(\d+)([pnu])(\d+)?$") @@ -44,11 +44,11 @@ def value_to_code(ohms: float) -> str: def parse_resistance_to_ohms(value: str, unit: Optional[str]) -> Optional[float]: if value is None: return None - s = str(value).strip().replace(" ", "").replace("Ω","ohm").replace("Ω","ohm") + s = str(value).strip().replace(" ", "").replace(",", "").replace("Ω","ohm").replace("Ω","ohm") m = RE_RES_SIMPLE.fullmatch(s) if m and unit is None: num = float(m.group(1)); u = (m.group(2) or "").lower() - table = {"ohm":1.0, "ohms":1.0, "r":1.0, "k":1e3, "kohm":1e3, "kω":1e3, "m":1e6, "mohm":1e6, "mω":1e6} + table = {"ohm":1.0, "ohms":1.0, "r":1.0, "k":1e3, "kohm":1e3, "kohms":1e3, "kω":1e3, "m":1e6, "mohm":1e6, "mohms":1e6, "mω":1e6} return num * table.get(u, 1.0) m = RE_RES_LETTER.fullmatch(s) if m and unit is None: @@ -60,7 +60,7 @@ def parse_resistance_to_ohms(value: str, unit: Optional[str]) -> Optional[float] num = float(s) if unit is None: return num u = str(unit).strip().lower() - table = {"ohm":1.0, "ohms":1.0, "r":1.0, "k":1e3, "kohm":1e3, "m":1e6, "mohm":1e6} + table = {"ohm":1.0, "ohms":1.0, "r":1.0, "k":1e3, "kohm":1e3, "kohms":1e3, "m":1e6, "mohm":1e6, "mohms":1e6} mul = table.get(u); return num * mul if mul else None except ValueError: @@ -85,7 +85,7 @@ def format_ohms_for_eda(ohms: float) -> str: def parse_capacitance_to_farads(value: str, unit: Optional[str]) -> Optional[float]: if value is None: return None - s = str(value).strip().replace(" ", "").replace("µ","u").replace("μ","u") + s = str(value).strip().replace(" ", "").replace(",", "").replace("µ","u").replace("μ","u") m = RE_CAP_SIMPLE.fullmatch(s) if m and unit is None: num = float(m.group(1)); su = (m.group(2) or "f").lower() diff --git a/partdb_cookies.json b/partdb_cookies.json index e69de29..8a0facd 100644 --- a/partdb_cookies.json +++ b/partdb_cookies.json @@ -0,0 +1 @@ +[{"name": "PHPSESSID", "value": "5c79b03879c439fa0991d88e4d5206b1", "path": "/", "domain": "partdb.neutronservices.duckdns.org", "secure": true, "httpOnly": true, "sameSite": "Lax"}] \ No newline at end of file diff --git a/provider/selenium_flow.py b/provider/selenium_flow.py index 1e6ac90..2a235db 100644 --- a/provider/selenium_flow.py +++ b/provider/selenium_flow.py @@ -663,6 +663,218 @@ def set_eda_from_capacitance(api: PartDB, part_id: int, *, max_wait_s: int = 12, if time.time() >= deadline: return False time.sleep(poll_every) +def accept_bulk_import_jobs(driver, base_url: str, lang: str, job_url: str = None, max_iterations: int = 100) -> Tuple[int, int, int]: + """ + Automates accepting bulk import jobs by: + 1. Finding and marking skipped parts (cards with "0 results found") as pending + 2. Finding the first selectable "Update part" button in a border-success card + 3. Clicking it and waiting for page load + 4. Clicking "Save" and waiting for page load + 5. Clicking "Save" again and waiting for page load + 6. Clicking "Complete" and waiting for page load + 7. Repeating until no more selectable buttons exist + + Returns (successful_count, failed_count, skipped_count) + """ + # Navigate to the job/import page if URL provided + if job_url: + driver.get(job_url) + time.sleep(1.5) + + successful = 0 + failed = 0 + total_skipped = 0 + + for iteration in range(max_iterations): + print(f"\n[Accept Jobs] Iteration {iteration + 1}/{max_iterations}") + + # Scroll to top first to ensure we see all buttons + try: + driver.execute_script("window.scrollTo(0, 0);") + time.sleep(0.5) + except Exception: + pass + + # First check for skipped parts (border-warning cards with "0 results found" or "No results found") + # and mark them as skipped by clicking "Mark Pending" + skipped_count = 0 + try: + # Find cards with border-warning that have "0 results found" badge or "No results found" alert + warning_cards = driver.find_elements(By.XPATH, "//div[contains(@class, 'card') and contains(@class, 'border-warning')]") + + for card in warning_cards: + try: + # Check if it has "0 results found" badge or "No results found" message + has_no_results = False + try: + badge = card.find_element(By.XPATH, ".//span[contains(@class, 'badge') and contains(@class, 'bg-info') and contains(., 'results found')]") + if badge and '0 results' in badge.text: + has_no_results = True + except Exception: + pass + + if not has_no_results: + try: + alert = card.find_element(By.XPATH, ".//div[contains(@class, 'alert-info') and contains(., 'No results found')]") + if alert: + has_no_results = True + except Exception: + pass + + if has_no_results: + # This card should be skipped, click "Mark Skipped" button if available + try: + mark_skipped_btn = card.find_element(By.XPATH, ".//button[contains(., 'Mark Skipped')]") + if mark_skipped_btn and mark_skipped_btn.is_displayed(): + driver.execute_script("arguments[0].scrollIntoView({block:'center', behavior:'smooth'});", mark_skipped_btn) + time.sleep(0.3) + try: + mark_skipped_btn.click() + except Exception: + driver.execute_script("arguments[0].click();", mark_skipped_btn) + skipped_count += 1 + print(f"[Accept Jobs] Marked card as skipped (no results found)") + time.sleep(0.5) + except Exception: + pass + except Exception as e: + continue + + if skipped_count > 0: + print(f"[Accept Jobs] Marked {skipped_count} cards as pending (no results)") + total_skipped += skipped_count + time.sleep(1.0) # Wait after marking items as pending + except Exception as e: + print(f"[Accept Jobs] Error checking for skipped cards: {e}") + + # Find all "Update part" buttons that are NOT disabled (no 'disabled' in class) + update_button = None + try: + # Find or