""" FileCabinet CS - Batch Export Automation (Step 2) ================================================= Reads a list of drawer IDs from a file, then for each drawer: 1. Navigates to it in the main window (type ID -> Go) 2. Opens File > Send To > File 3. Clicks "Select ->" (grabs all documents via the tree root) 4. Clicks OK 5. Waits for the "Exporting Documents" progress dialog to appear and finish 6. Logs success/failure and moves to the next drawer Key safety features: - RESUME: completed drawer IDs are appended to a progress log. On restart, already-completed drawers are skipped. A multi-hour run that dies partway can simply be re-run and it picks up where it left off. - LOGGING: every action and error is timestamped to a log file. - DEFENSIVE: unexpected states are caught, logged, screenshotted, and the run continues with the next drawer rather than crashing. PREREQUISITES: - Run from 32-bit Python (matches FileCabinet CS's architecture) - FileCabinet CS already open, pointed at the correct data location - Export options/checkboxes and destination already set MANUALLY once (they persist between exports within a session) - Neither FileCabinet nor this script running elevated (they must match) """ import time import sys import os from pywinauto import Desktop, Application from pywinauto import timings from fccs_config import parse_args, load_config, make_logger, load_lines # --------------------------------------------------------------------------- # HELPERS # --------------------------------------------------------------------------- def load_completed(path): """Read the set of already-completed drawer IDs for resume support.""" if not os.path.exists(path): return set() with open(path, "r", encoding="utf-8") as f: return {line.strip() for line in f if line.strip()} def mark_completed(path, drawer_id): """Append a drawer ID to the completed log (resume tracking).""" with open(path, "a", encoding="utf-8") as f: f.write(drawer_id + "\n") def take_screenshot(window, drawer_id, tag, screenshot_dir, log): """Save a screenshot of a window for later diagnosis of a failure.""" from datetime import datetime try: os.makedirs(screenshot_dir, exist_ok=True) fname = os.path.join( screenshot_dir, f"{drawer_id}_{tag}_{datetime.now():%Y%m%d_%H%M%S}.png" ) window.capture_as_image().save(fname) log(f" Screenshot saved: {fname}") except Exception as e: log(f" Could not capture screenshot: {e}") # --------------------------------------------------------------------------- # CONNECTION # --------------------------------------------------------------------------- def connect_main(ctrl, log): """Find and connect to the running FileCabinet CS main window.""" target = None for w in Desktop(backend="win32").windows(): if w.class_name() == ctrl["main_class"]: target = w break if target is None: log("ERROR: FileCabinet CS window not found. Is it open?") sys.exit(1) app = Application(backend="win32").connect(handle=target.handle) main = app.window(handle=target.handle) log(f"Connected to: {main.window_text()}") return app, main # --------------------------------------------------------------------------- # PER-DRAWER EXPORT # --------------------------------------------------------------------------- def navigate_to_drawer(main, drawer_id, ctrl, timeouts, log): """Type the drawer ID into the search box and click Go.""" # Use best_match name "Edit" instead of title — the title is placeholder # text that changes once the user types in the box. box = main["Edit"] box.wait("visible ready", timeout=timeouts["nav"]) box.set_edit_text("") box.set_edit_text(drawer_id) time.sleep(timeouts["settle"]) main.child_window(title=ctrl["go_button_title"], class_name="Button").click() deadline = time.time() + timeouts["nav"] # Whatever drawer your are actively on becomes the actual name of the App Window. We are checking that again to make sure we found the drawer. while time.time() < deadline: title = main.window_text() if f"[{drawer_id}" in title: log(f" Navigated to drawer {drawer_id}: {title}") return True time.sleep(0.3) log(f" WARNING: window title did not confirm drawer {drawer_id} " f"(title is: {main.window_text()!r})") return False def open_send_to_file(main, app, ctrl, timeouts): """Open File > Send To > File and return the dialog window object.""" main.menu_select("File->Send To->File") dlg = app.window(title=ctrl["dialog_title"], class_name=ctrl["dialog_class"]) dlg.wait("visible ready", timeout=timeouts["dialog"]) return dlg def perform_export(dlg, ctrl, timeouts): """Click Select -> then OK to start the export.""" # Wait for the Select button to be ready — the dialog may still be # populating its controls after appearing. select_btn = dlg.child_window(title=ctrl["select_btn_title"], class_name="Button") select_btn.wait("visible ready enabled", timeout=timeouts["dialog"]) select_btn.click() # Wait for the OK button to become ready — transferring files to the # selected side can take a while on large drawers. ok_btn = dlg.child_window(title=ctrl["ok_btn_title"], class_name="Button") ok_btn.wait("visible ready enabled", timeout=timeouts["dialog"]) ok_btn.click() def dismiss_any_dialog(app, ctrl, log): """Try to find and dismiss any stale confirmation or error dialog.""" try: saved = app.window(title=ctrl["saved_title"], class_name=ctrl["saved_class"]) if saved.exists(timeout=1): saved.child_window(title=ctrl["saved_ok_title"], class_name="Button").click() log(" Dismissed stale confirmation dialog.") time.sleep(0.5) except Exception: pass # Also try to dismiss any generic #32770 dialog with an OK button try: for w in app.windows(): if w.class_name() == "#32770" and w.is_visible(): try: w.child_window(title="OK", class_name="Button").click() log(f" Dismissed stale dialog: {w.window_text()!r}") time.sleep(0.5) except Exception: pass except Exception: pass def wait_for_export(app, ctrl, timeouts, log): """ Wait for the full export lifecycle to complete: 1. 'Exporting Documents' progress dialog appears (export running) 2. Progress dialog disappears (export finished writing files) 3. '"Send to" file saved' confirmation dialog appears (MODAL) 4. Dismiss it by clicking OK """ progress = app.window(title=ctrl["progress_title"], class_name=ctrl["progress_class"]) saved = app.window(title=ctrl["saved_title"], class_name=ctrl["saved_class"]) try: progress.wait("visible", timeout=timeouts["progress_appear"]) log(" Export in progress...") except timings.TimeoutError: log(" NOTE: progress dialog not seen; checking for confirmation dialog " "(drawer may have exported very quickly).") # Poll until the confirmation dialog appears OR we time out. # We do NOT rely on wait_not for the progress dialog because it can # briefly disappear and reappear during long exports. deadline = time.time() + timeouts["progress_finish"] while time.time() < deadline: # Check if the confirmation dialog has appeared if saved.exists(timeout=0): break time.sleep(2) else: log(f" ERROR: export did not finish within {timeouts['progress_finish']}s.") return False try: saved.wait("visible ready", timeout=timeouts["confirm"]) log(" Confirmation dialog appeared - export succeeded.") except timings.TimeoutError: # Fallback: look for any #32770 dialog that appeared (title may differ) log(" WARNING: expected confirmation dialog not found by title. " "Checking for any popup dialog...") found = False try: for w in app.windows(): if w.class_name() == "#32770" and w.is_visible(): log(f" Found dialog: {w.window_text()!r}") try: w.child_window(title="OK", class_name="Button").click() w.wait_not("visible", timeout=10) log(" Dismissed via fallback.") found = True break except Exception: pass except Exception: pass if not found: log(" ERROR: confirmation dialog never appeared. Export may have " "failed or produced no output.") return False log(" Export complete.") return True try: saved.child_window(title=ctrl["saved_ok_title"], class_name="Button").click() saved.wait_not("visible", timeout=timeouts["confirm"]) log(" Confirmation dismissed.") except Exception as e: log(f" WARNING: could not dismiss confirmation dialog: {e}") log(" Export succeeded but dialog remains — will be dismissed next iteration.") log(" Export complete.") return True def export_drawer(app, main, drawer_id, ctrl, timeouts, screenshot_dir, log): """Full per-drawer sequence. Returns True on success.""" log(f"Drawer {drawer_id}: starting") # 0. Dismiss any stale dialogs left from a previous failure dismiss_any_dialog(app, ctrl, log) # 1. Navigate if not navigate_to_drawer(main, drawer_id, ctrl, timeouts, log): take_screenshot(main, drawer_id, "nav_fail", screenshot_dir, log) return False # 2. Open Send To File dialog try: dlg = open_send_to_file(main, app, ctrl, timeouts) except Exception as e: log(f" ERROR opening Send To File dialog: {e}") take_screenshot(main, drawer_id, "dialog_fail", screenshot_dir, log) return False # 3 & 4. Select -> and OK try: perform_export(dlg, ctrl, timeouts) except Exception as e: log(f" ERROR during export selection: {e}") take_screenshot(dlg, drawer_id, "export_fail", screenshot_dir, log) try: dlg.child_window(title="Cancel", class_name="Button").click() except Exception: pass return False # 5. Monitor progress ok = wait_for_export(app, ctrl, timeouts, log) if not ok: take_screenshot(main, drawer_id, "progress_fail", screenshot_dir, log) return False time.sleep(timeouts["settle"]) return True # --------------------------------------------------------------------------- # MAIN LOOP # --------------------------------------------------------------------------- def main(): args = parse_args() cfg = load_config(args.config) log = make_logger(cfg.get("paths", "log_file")) # Build lookup dicts from config paths = { "drawer_id_file": cfg.get("paths", "drawer_id_file"), "completed_file": cfg.get("paths", "completed_file"), "screenshot_dir": cfg.get("paths", "screenshot_dir"), } timeouts = { "nav": cfg.getfloat("timeouts", "nav_timeout"), "dialog": cfg.getfloat("timeouts", "dialog_timeout"), "progress_appear": cfg.getfloat("timeouts", "progress_appear"), "progress_finish": cfg.getfloat("timeouts", "progress_finish"), "settle": cfg.getfloat("timeouts", "settle"), "confirm": cfg.getfloat("timeouts", "confirm_timeout"), } ctrl = { "main_class": cfg.get("controls", "main_class"), "drawer_box_title": cfg.get("controls", "drawer_box_title"), "go_button_title": cfg.get("controls", "go_button_title"), "dialog_title": cfg.get("controls", "dialog_title"), "dialog_class": cfg.get("controls", "dialog_class"), "select_btn_title": cfg.get("controls", "select_btn_title"), "ok_btn_title": cfg.get("controls", "ok_btn_title"), "progress_title": cfg.get("controls", "progress_title"), "progress_class": cfg.get("controls", "progress_class"), "saved_title": cfg.get("controls", "saved_title"), "saved_class": cfg.get("controls", "saved_class"), "saved_ok_title": cfg.get("controls", "saved_ok_title"), } log("=" * 60) log("FileCabinet CS batch export - run starting") log("=" * 60) all_ids = load_lines(paths["drawer_id_file"]) if not all_ids: log(f"ERROR: no drawer IDs found in {paths['drawer_id_file']}") sys.exit(1) completed = load_completed(paths["completed_file"]) pending = [d for d in all_ids if d not in completed] log(f"Total drawers in list : {len(all_ids)}") log(f"Already completed : {len(completed)}") log(f"Remaining to process : {len(pending)}") if not pending: log("Nothing to do - all drawers already completed.") return app, main_win = connect_main(ctrl, log) success_count = 0 fail_count = 0 failed_ids = [] for i, drawer_id in enumerate(pending, start=1): log(f"--- [{i}/{len(pending)}] Drawer {drawer_id} ---") try: ok = export_drawer(app, main_win, drawer_id, ctrl, timeouts, paths["screenshot_dir"], log) except Exception as e: log(f" UNEXPECTED ERROR on drawer {drawer_id}: {e}") try: take_screenshot(main_win, drawer_id, "unexpected", paths["screenshot_dir"], log) except Exception: pass ok = False if ok: mark_completed(paths["completed_file"], drawer_id) success_count += 1 log(f" Drawer {drawer_id} DONE ({success_count} ok / " f"{fail_count} failed so far)") else: fail_count += 1 failed_ids.append(drawer_id) log(f" Drawer {drawer_id} FAILED - will NOT be marked complete. " f"Re-run later to retry.") log("=" * 60) log("Run finished.") log(f" Succeeded : {success_count}") log(f" Failed : {fail_count}") if failed_ids: log(f" Failed drawer IDs: {', '.join(failed_ids)}") log(" These were NOT marked complete; re-running the script " "will retry them.") log("=" * 60) if __name__ == "__main__": main()