162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
"""
|
|
Utility: Check whether specific drawers actually finished exporting.
|
|
|
|
Interactive — prompts for one or more drawer IDs, then for each drawer compares
|
|
its manifest (the documents FCCS said it would export, captured during Step 2)
|
|
against the files actually sitting in the export folder, and reports any
|
|
missing documents.
|
|
|
|
Handles two quirks of the FCCS export:
|
|
|
|
1. Page-splitting: a single manifest document (e.g. "Donations") is exported
|
|
as one file if it's a single page, or as "Donations Page 1",
|
|
"Donations Page 2", ... for a multi-page document. All of those count as
|
|
that one document being present.
|
|
|
|
2. Filename sanitization: FCCS strips characters that are illegal in Windows
|
|
filenames (: / \\ ? * " < > |) from document titles, so a manifest title
|
|
like 'US Tax Return (... 01:35PM)' won't match the exported file
|
|
character-for-character. Comparison is done on a normalized key
|
|
(lowercase, alphanumerics only) so these still match.
|
|
|
|
USAGE
|
|
-----
|
|
python fccs_check.py
|
|
Drawer ID(s): 08097 18430
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
from fccs_config import parse_args, load_config
|
|
from fccs_verify import load_manifest # reuse the both-format manifest loader
|
|
|
|
|
|
# Trailing " Page N" (optionally "Page N of M") appended to multi-page exports.
|
|
_PAGE_RE = re.compile(r"\s*Page\s+\d+(?:\s+of\s+\d+)?\s*$", re.IGNORECASE)
|
|
# Creation-date field ("_MM-DD-YYYY_") that precedes the document name.
|
|
_DATE_ANCHOR = re.compile(r"_\d{2}-\d{2}-\d{4}_")
|
|
|
|
|
|
def match_key(name):
|
|
"""Normalize a document name for tolerant comparison.
|
|
|
|
Strips a trailing 'Page N' page-split suffix, then reduces to lowercase
|
|
alphanumerics so punctuation and filename-sanitization differences don't
|
|
cause false mismatches.
|
|
"""
|
|
base = _PAGE_RE.sub("", name)
|
|
return re.sub(r"[^a-z0-9]+", "", base.lower())
|
|
|
|
|
|
def manifest_doc_names(path, drawer_id):
|
|
"""Return the expected document (Page Title) names from a manifest file."""
|
|
rows = load_manifest(path)
|
|
names = []
|
|
for row in rows:
|
|
if len(row) >= 3 and row[0].strip() == drawer_id:
|
|
names.append(row[1]) # old format: DrawerID, PageTitle, Application
|
|
elif row:
|
|
names.append(row[0]) # new format: PageTitle, Application
|
|
return names
|
|
|
|
|
|
def exported_doc_name(filename):
|
|
"""Extract the document-name portion from an exported filename, or None.
|
|
|
|
Format: {drawer}_{client}_{folder}_{MM-DD-YYYY}_{docname}.ext
|
|
The creation-date field is a reliable anchor; the doc name follows the last
|
|
one (the client/folder fields don't carry an "_MM-DD-YYYY_" pattern).
|
|
"""
|
|
stem = os.path.splitext(filename)[0]
|
|
anchors = list(_DATE_ANCHOR.finditer(stem))
|
|
if not anchors:
|
|
return None
|
|
return stem[anchors[-1].end():]
|
|
|
|
|
|
def check_drawer(drawer_id, files, manifest_dir, out):
|
|
"""Report completeness of one drawer's export."""
|
|
manifest_path = os.path.join(manifest_dir, drawer_id + ".txt")
|
|
if not os.path.exists(manifest_path):
|
|
out("")
|
|
out(f"[{drawer_id}] NO MANIFEST at {manifest_path} — cannot verify "
|
|
"(was this drawer exported by the tool?)")
|
|
return
|
|
|
|
expected = manifest_doc_names(manifest_path, drawer_id)
|
|
|
|
# Group exported files by normalized doc name; page-splits collapse together.
|
|
exported = {} # key -> list of full doc names (one entry per file/page)
|
|
unparsed = []
|
|
for f in files:
|
|
doc = exported_doc_name(f)
|
|
if doc is None:
|
|
unparsed.append(f)
|
|
continue
|
|
exported.setdefault(match_key(doc), []).append(doc)
|
|
|
|
missing = [name for name in expected if match_key(name) not in exported]
|
|
|
|
expected_keys = {match_key(n) for n in expected}
|
|
extras = [names[0] for k, names in exported.items() if k not in expected_keys]
|
|
|
|
out("")
|
|
out(f"[{drawer_id}] manifest lists {len(expected)} document(s); "
|
|
f"{len(files)} file(s) in export folder.")
|
|
if missing:
|
|
out(f" INCOMPLETE — {len(missing)} document(s) missing from export:")
|
|
for m in missing:
|
|
out(f" - {m}")
|
|
else:
|
|
out(f" COMPLETE — all {len(expected)} manifest document(s) present.")
|
|
if extras:
|
|
out(f" Note: {len(extras)} exported document(s) not in the manifest:")
|
|
for e in extras:
|
|
out(f" - {e}")
|
|
if unparsed:
|
|
out(f" Note: {len(unparsed)} file(s) had no recognizable date anchor "
|
|
"and were skipped.")
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
cfg = load_config(args.config)
|
|
export_dir = cfg.get("paths", "export_dir")
|
|
manifest_dir = cfg.get("paths", "manifest_dir")
|
|
|
|
if not os.path.isdir(export_dir):
|
|
print(f"ERROR: export directory not found: {export_dir}")
|
|
sys.exit(1)
|
|
|
|
# Index every export file by its leading drawer-ID token (before first "_").
|
|
# The underscore boundary keeps clashing IDs separate (04289 vs 04289TS).
|
|
files_by_drawer = {}
|
|
for f in os.listdir(export_dir):
|
|
if not os.path.isfile(os.path.join(export_dir, f)):
|
|
continue
|
|
token = f.split("_", 1)[0]
|
|
files_by_drawer.setdefault(token, []).append(f)
|
|
|
|
print("FCCS export completeness check")
|
|
print(f" export folder : {export_dir}")
|
|
print(f" manifests : {manifest_dir}")
|
|
print("Enter drawer ID(s) separated by spaces or commas (blank to quit).")
|
|
|
|
while True:
|
|
try:
|
|
raw = input("\nDrawer ID(s): ").strip()
|
|
except EOFError:
|
|
break
|
|
if not raw:
|
|
break
|
|
ids = [i for i in re.split(r"[\s,]+", raw) if i]
|
|
for drawer_id in ids:
|
|
check_drawer(drawer_id, files_by_drawer.get(drawer_id, []),
|
|
manifest_dir, print)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|