Files
FCCS/fccs_export.py

387 lines
14 KiB
Python
Executable File

"""
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" ERROR dismissing confirmation dialog: {e}")
return False
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()