added manifests to create a list of expected file outputs as well as a verification script.

This commit is contained in:
2026-07-08 20:33:44 -05:00
parent d02333f9f9
commit 9303bcf0b5
4 changed files with 172 additions and 10 deletions

View File

@@ -19,6 +19,7 @@ Extracts and organizes documents from FileCabinet CS (Thomson Reuters) using GUI
| `fccs_scan.py` | Step 1: Scan backup directory for drawer IDs |
| `fccs_export.py` | Step 2: Automate FCCS GUI to export all drawers |
| `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure |
| `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files |
## Setup Per Engagement
@@ -47,6 +48,14 @@ Requires FCCS to be open with export destination already configured. Automates t
- **Screenshots** -- captures failure states for diagnosis
- **Defensive** -- one bad drawer won't crash the entire run
### Step 2b (optional): Verify Export Completeness
```
python fccs_verify.py
```
During export, each drawer's document list is captured from the FCCS dialog and saved as a manifest. This script compares those manifests against the actual exported files to flag any drawers with missing or extra files.
### Step 3: Reorganize Files
```

View File

@@ -7,6 +7,7 @@ drawer_id_file = C:\Migration\drawer_ids.txt
completed_file = C:\Migration\completed.txt
log_file = C:\Migration\run_log.txt
screenshot_dir = C:\Migration\screenshots
manifest_dir = C:\Migration\manifests
folder_list = fccs_folders.txt
[timeouts]

View File

@@ -129,19 +129,48 @@ def open_send_to_file(main, app, ctrl, timeouts):
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.
def read_manifest(dlg, log):
"""Read the list of selected documents from the right-side ListView."""
try:
lv = dlg.child_window(title="List1", class_name="SysListView32")
count = lv.item_count()
if count == 0:
log(" WARNING: ListView is empty after Select")
return []
items = lv.texts()
log(f" Manifest captured: {count} documents selected")
return items
except Exception as e:
log(f" WARNING: could not read manifest from ListView: {e}")
return None
def save_manifest(drawer_id, items, manifest_dir, log):
"""Save the list of expected export items to a manifest file."""
os.makedirs(manifest_dir, exist_ok=True)
path = os.path.join(manifest_dir, f"{drawer_id}.txt")
with open(path, "w", encoding="utf-8") as f:
for row in items:
if isinstance(row, (list, tuple)):
f.write("\t".join(str(c) for c in row) + "\n")
else:
f.write(str(row) + "\n")
log(f" Manifest saved: {path}")
def perform_export(dlg, ctrl, timeouts, log):
"""Click Select -> then OK to start the export. Returns manifest list."""
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["select"])
manifest = read_manifest(dlg, log)
ok_btn.click()
return manifest
def dismiss_any_dialog(app, ctrl, log):
@@ -228,7 +257,8 @@ def wait_for_export(app, ctrl, timeouts, log):
return True
def export_drawer(app, main, drawer_id, ctrl, timeouts, screenshot_dir, log):
def export_drawer(app, main, drawer_id, ctrl, timeouts, screenshot_dir,
manifest_dir, log):
"""Full per-drawer sequence. Returns True on success."""
log(f"Drawer {drawer_id}: starting")
@@ -248,9 +278,11 @@ def export_drawer(app, main, drawer_id, ctrl, timeouts, screenshot_dir, log):
take_screenshot(main, drawer_id, "dialog_fail", screenshot_dir, log)
return False
# 3 & 4. Select -> and OK
# 3 & 4. Select -> and OK (captures manifest before clicking OK)
try:
perform_export(dlg, ctrl, timeouts)
manifest = perform_export(dlg, ctrl, timeouts, log)
if manifest is not None:
save_manifest(drawer_id, manifest, manifest_dir, log)
except Exception as e:
log(f" ERROR during export selection: {e}")
take_screenshot(dlg, drawer_id, "export_fail", screenshot_dir, log)
@@ -284,6 +316,7 @@ def main():
"drawer_id_file": cfg.get("paths", "drawer_id_file"),
"completed_file": cfg.get("paths", "completed_file"),
"screenshot_dir": cfg.get("paths", "screenshot_dir"),
"manifest_dir": cfg.get("paths", "manifest_dir"),
}
timeouts = {
"nav": cfg.getfloat("timeouts", "nav_timeout"),
@@ -339,7 +372,8 @@ def main():
log(f"--- [{i}/{len(pending)}] Drawer {drawer_id} ---")
try:
ok = export_drawer(app, main_win, drawer_id, ctrl, timeouts,
paths["screenshot_dir"], log)
paths["screenshot_dir"],
paths["manifest_dir"], log)
except Exception as e:
log(f" UNEXPECTED ERROR on drawer {drawer_id}: {e}")
try:

118
fccs_verify.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Step 4 (optional): Verify exported files against manifests.
Compares the per-drawer manifests captured during export (Step 2)
against the actual files in the export directory to identify
missing or extra files.
"""
import os
import sys
from fccs_config import parse_args, load_config, make_logger
def load_manifest(path):
"""Load a manifest file and return the list of document rows."""
items = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.rstrip("\n")
if line:
items.append(line.split("\t"))
return items
def main():
args = parse_args()
cfg = load_config(args.config)
log = make_logger(cfg.get("paths", "log_file"))
export_dir = cfg.get("paths", "export_dir")
manifest_dir = cfg.get("paths", "manifest_dir")
if not os.path.isdir(manifest_dir):
log(f"ERROR: manifest directory not found: {manifest_dir}")
log("Run fccs_export.py first to generate manifests.")
sys.exit(1)
if not os.path.isdir(export_dir):
log(f"ERROR: export directory not found: {export_dir}")
sys.exit(1)
# Load all manifests
manifests = {}
for fname in sorted(os.listdir(manifest_dir)):
if fname.endswith(".txt"):
drawer_id = os.path.splitext(fname)[0]
items = load_manifest(os.path.join(manifest_dir, fname))
manifests[drawer_id] = items
if not manifests:
log("No manifest files found.")
sys.exit(1)
# Index exported files by drawer ID prefix
exported_by_drawer = {}
all_exported = [
f for f in os.listdir(export_dir)
if os.path.isfile(os.path.join(export_dir, f))
]
for fname in all_exported:
sep = fname.find("_")
if sep != -1:
did = fname[:sep]
exported_by_drawer.setdefault(did, []).append(fname)
log("=" * 60)
log("Export Verification Report")
log("=" * 60)
log(f"Manifests loaded : {len(manifests)} drawers")
log(f"Exported files : {len(all_exported)} total")
log("")
total_expected = 0
total_actual = 0
mismatched = []
for drawer_id in sorted(manifests):
expected = manifests[drawer_id]
actual = exported_by_drawer.get(drawer_id, [])
n_expected = len(expected)
n_actual = len(actual)
total_expected += n_expected
total_actual += n_actual
if n_expected == n_actual:
log(f" {drawer_id}: OK ({n_actual} files)")
else:
diff = n_actual - n_expected
sign = "+" if diff > 0 else ""
log(f" {drawer_id}: MISMATCH — expected {n_expected}, "
f"got {n_actual} ({sign}{diff})")
mismatched.append(drawer_id)
# Check for exported files with no manifest
orphan_drawers = set(exported_by_drawer.keys()) - set(manifests.keys())
orphan_count = sum(len(exported_by_drawer[d]) for d in orphan_drawers)
log("")
log("-" * 60)
log(f"Expected total : {total_expected}")
log(f"Actual total : {total_actual}")
if orphan_drawers:
log(f"No manifest for : {', '.join(sorted(orphan_drawers))} "
f"({orphan_count} files)")
if mismatched:
log(f"Mismatched drawers: {', '.join(mismatched)}")
elif not orphan_drawers:
log("All drawers match.")
log("=" * 60)
if __name__ == "__main__":
main()