Initial commit: FCCS extraction tool

This commit is contained in:
2026-07-05 22:38:38 -05:00
commit 0da73f6c6e
6 changed files with 614 additions and 0 deletions

320
fccs_export.py Executable file
View File

@@ -0,0 +1,320 @@
"""
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."""
box = main.child_window(title=ctrl["drawer_box_title"], class_name="Edit")
box.wait("visible ready", timeout=timeouts["nav"])
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"]
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."""
dlg.child_window(title=ctrl["select_btn_title"], class_name="Button").click()
time.sleep(timeouts["settle"])
dlg.child_window(title=ctrl["ok_btn_title"], class_name="Button").click()
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"])
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).")
try:
progress.wait_not("visible", timeout=timeouts["progress_finish"])
except timings.TimeoutError:
log(f" ERROR: export did not finish within {timeouts['progress_finish']}s.")
return False
saved = app.window(title=ctrl["saved_title"], class_name=ctrl["saved_class"])
try:
saved.wait("visible ready", timeout=timeouts["confirm"])
log(" Confirmation dialog appeared - export succeeded.")
except timings.TimeoutError:
log(" ERROR: confirmation dialog never appeared. Export may have "
"failed or produced no output.")
return False
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")
# 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()