updated our verify logic
This commit is contained in:
42
README.md
42
README.md
@@ -61,22 +61,6 @@ Requires FCCS to be open with export destination already configured. Automates t
|
|||||||
- **Defensive** -- one bad drawer won't crash the entire run
|
- **Defensive** -- one bad drawer won't crash the entire run
|
||||||
- **Crash recovery** -- some documents (e.g. UltraTax "Diagnostics" files) crash FCCS's converter (`FileConversionEngine::convert() failed`), which aborts that drawer's export. The script detects the error dialog, screenshots and logs the crashing document, dismisses it, and records the drawer in `crashed.txt` so it's skipped on future runs instead of stalling. Handle crashed drawers manually (export them excluding the poison document); delete a line from `crashed.txt` to retry after fixing.
|
- **Crash recovery** -- some documents (e.g. UltraTax "Diagnostics" files) crash FCCS's converter (`FileConversionEngine::convert() failed`), which aborts that drawer's export. The script detects the error dialog, screenshots and logs the crashing document, dismisses it, and records the drawer in `crashed.txt` so it's skipped on future runs instead of stalling. Handle crashed drawers manually (export them excluding the poison document); delete a line from `crashed.txt` to retry after fixing.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
To spot-check specific drawers (e.g. ones the log marked failed, to see whether they actually finished exporting in the background), run:
|
|
||||||
|
|
||||||
```
|
|
||||||
python fccs_check.py
|
|
||||||
```
|
|
||||||
|
|
||||||
It prompts for one or more drawer IDs and reports, per drawer, which manifest documents are present vs missing. It accounts for FCCS page-splitting (a document exported as `Name Page 1`, `Name Page 2`, … counts as present) and for filename sanitization (titles containing characters illegal in filenames, like `:`, still match).
|
|
||||||
|
|
||||||
### Step 3: Reorganize Files
|
### Step 3: Reorganize Files
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -105,6 +89,32 @@ output/
|
|||||||
|
|
||||||
Exported filenames follow the format `{drawer_id}_{client_name}_{folder_name}_{creation_date}_{document_name}.ext`. The parser uses folder templates from `fccs_folders.txt` (with `YYYY` expanded via regex) and the creation date (`MM-DD-YYYY`) as anchors to reliably split the underscore-delimited fields. Folder names are decomposed into nested paths that match the FCCS UI structure (e.g. `2025 Tax Documents` becomes `Tax Documents/2025/`). UltraTax CS folders are matched by a built-in pattern.
|
Exported filenames follow the format `{drawer_id}_{client_name}_{folder_name}_{creation_date}_{document_name}.ext`. The parser uses folder templates from `fccs_folders.txt` (with `YYYY` expanded via regex) and the creation date (`MM-DD-YYYY`) as anchors to reliably split the underscore-delimited fields. Folder names are decomposed into nested paths that match the FCCS UI structure (e.g. `2025 Tax Documents` becomes `Tax Documents/2025/`). UltraTax CS folders are matched by a built-in pattern.
|
||||||
|
|
||||||
|
### Step 4 (optional): Verify Export Completeness
|
||||||
|
|
||||||
|
During export, each drawer's document list is captured from the FCCS dialog and saved as a manifest (in `manifest_dir`). These tools compare the manifests against the files actually in the export folder to confirm nothing was missed.
|
||||||
|
|
||||||
|
Both compare at the **document level** and share identical matching logic. They account for:
|
||||||
|
|
||||||
|
- **Page-splitting** -- a multi-page document exported as `Name Page 1`, `Name Page 2`, … counts as that one document being present.
|
||||||
|
- **Filename sanitization** -- document titles containing characters illegal in Windows filenames (e.g. `:` `/` `?`) still match the exported files.
|
||||||
|
|
||||||
|
Batch-check every drawer that has a manifest:
|
||||||
|
|
||||||
|
```
|
||||||
|
python fccs_verify.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Reports each drawer as `OK` or `INCOMPLETE` (listing the missing documents), plus a summary and any exported drawers that have no manifest.
|
||||||
|
|
||||||
|
Spot-check specific drawers interactively (e.g. ones the log marked failed, to see whether they actually finished exporting in the background):
|
||||||
|
|
||||||
|
```
|
||||||
|
python fccs_check.py
|
||||||
|
Drawer ID(s): 08097 18430
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: because page-splitting means the number of files can't be mapped one-to-one to documents, completeness is judged by document *presence* (is each manifest document represented by at least one exported file), not by exact file counts.
|
||||||
|
|
||||||
## Config Reference
|
## Config Reference
|
||||||
|
|
||||||
All scripts read from `config.ini` (or specify `--config path\to\config.ini`).
|
All scripts read from `config.ini` (or specify `--config path\to\config.ini`).
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
111
fccs_check.py
111
fccs_check.py
@@ -6,7 +6,7 @@ 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
|
against the files actually sitting in the export folder, and reports any
|
||||||
missing documents.
|
missing documents.
|
||||||
|
|
||||||
Handles two quirks of the FCCS export:
|
Handles two quirks of the FCCS export (see fccs_config.evaluate_drawer):
|
||||||
|
|
||||||
1. Page-splitting: a single manifest document (e.g. "Donations") is exported
|
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",
|
as one file if it's a single page, or as "Donations Page 1",
|
||||||
@@ -14,10 +14,11 @@ Handles two quirks of the FCCS export:
|
|||||||
that one document being present.
|
that one document being present.
|
||||||
|
|
||||||
2. Filename sanitization: FCCS strips characters that are illegal in Windows
|
2. Filename sanitization: FCCS strips characters that are illegal in Windows
|
||||||
filenames (: / \\ ? * " < > |) from document titles, so a manifest title
|
filenames (: / \\ ? * " < > |) from document titles, so comparison is done
|
||||||
like 'US Tax Return (... 01:35PM)' won't match the exported file
|
on a normalized key so these still match.
|
||||||
character-for-character. Comparison is done on a normalized key
|
|
||||||
(lowercase, alphanumerics only) so these still match.
|
fccs_verify.py applies this same logic in batch across every drawer; this tool
|
||||||
|
is for spot-checking specific drawers (e.g. ones the log marked failed).
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
-----
|
-----
|
||||||
@@ -29,94 +30,37 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from fccs_config import parse_args, load_config
|
from fccs_config import (
|
||||||
from fccs_verify import load_manifest # reuse the both-format manifest loader
|
parse_args, load_config, evaluate_drawer, index_files_by_drawer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Trailing " Page N" (optionally "Page N of M") appended to multi-page exports.
|
def report_drawer(drawer_id, files, manifest_dir, out):
|
||||||
_PAGE_RE = re.compile(r"\s*Page\s+\d+(?:\s+of\s+\d+)?\s*$", re.IGNORECASE)
|
"""Evaluate one drawer and print a human-readable completeness report."""
|
||||||
# Creation-date field ("_MM-DD-YYYY_") that precedes the document name.
|
r = evaluate_drawer(drawer_id, files, manifest_dir)
|
||||||
_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("")
|
||||||
out(f"[{drawer_id}] NO MANIFEST at {manifest_path} — cannot verify "
|
if not r["has_manifest"]:
|
||||||
|
out(f"[{drawer_id}] NO MANIFEST at {r['manifest_path']} — cannot verify "
|
||||||
"(was this drawer exported by the tool?)")
|
"(was this drawer exported by the tool?)")
|
||||||
return
|
return
|
||||||
|
|
||||||
expected = manifest_doc_names(manifest_path, drawer_id)
|
expected = r["expected"]
|
||||||
|
missing = r["missing"]
|
||||||
# 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); "
|
out(f"[{drawer_id}] manifest lists {len(expected)} document(s); "
|
||||||
f"{len(files)} file(s) in export folder.")
|
f"{r['file_count']} file(s) in export folder.")
|
||||||
if missing:
|
if missing:
|
||||||
out(f" INCOMPLETE — {len(missing)} document(s) missing from export:")
|
out(f" INCOMPLETE — {len(missing)} document(s) missing from export:")
|
||||||
for m in missing:
|
for m in missing:
|
||||||
out(f" - {m}")
|
out(f" - {m}")
|
||||||
else:
|
else:
|
||||||
out(f" COMPLETE — all {len(expected)} manifest document(s) present.")
|
out(f" COMPLETE — all {len(expected)} manifest document(s) present.")
|
||||||
if extras:
|
if r["extras"]:
|
||||||
out(f" Note: {len(extras)} exported document(s) not in the manifest:")
|
out(f" Note: {len(r['extras'])} exported document(s) not in the manifest:")
|
||||||
for e in extras:
|
for e in r["extras"]:
|
||||||
out(f" - {e}")
|
out(f" - {e}")
|
||||||
if unparsed:
|
if r["unparsed"]:
|
||||||
out(f" Note: {len(unparsed)} file(s) had no recognizable date anchor "
|
out(f" Note: {r['unparsed']} file(s) had no recognizable date anchor "
|
||||||
"and were skipped.")
|
"and were skipped.")
|
||||||
|
|
||||||
|
|
||||||
@@ -130,14 +74,7 @@ def main():
|
|||||||
print(f"ERROR: export directory not found: {export_dir}")
|
print(f"ERROR: export directory not found: {export_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Index every export file by its leading drawer-ID token (before first "_").
|
files_by_drawer = index_files_by_drawer(export_dir)
|
||||||
# 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("FCCS export completeness check")
|
||||||
print(f" export folder : {export_dir}")
|
print(f" export folder : {export_dir}")
|
||||||
@@ -153,7 +90,7 @@ def main():
|
|||||||
break
|
break
|
||||||
ids = [i for i in re.split(r"[\s,]+", raw) if i]
|
ids = [i for i in re.split(r"[\s,]+", raw) if i]
|
||||||
for drawer_id in ids:
|
for drawer_id in ids:
|
||||||
check_drawer(drawer_id, files_by_drawer.get(drawer_id, []),
|
report_drawer(drawer_id, files_by_drawer.get(drawer_id, []),
|
||||||
manifest_dir, print)
|
manifest_dir, print)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
132
fccs_config.py
132
fccs_config.py
@@ -157,3 +157,135 @@ def check_for_clashes(drawer_ids):
|
|||||||
if matches:
|
if matches:
|
||||||
clashes.append((short, matches))
|
clashes.append((short, matches))
|
||||||
return clashes
|
return clashes
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MANIFEST / EXPORT-COMPLETENESS HELPERS
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared by fccs_verify.py (batch) and fccs_check.py (interactive) so both
|
||||||
|
# judge completeness identically.
|
||||||
|
|
||||||
|
# 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 load_manifest(path):
|
||||||
|
"""Load a manifest file and return one row (list of cells) per document.
|
||||||
|
|
||||||
|
Handles both formats:
|
||||||
|
- New: one document per line (tab-separated columns).
|
||||||
|
- Old: a raw ListView dump led by 'List1', then row-major cells with
|
||||||
|
3 columns per document (Drawer ID, Page Title, Application), reshaped
|
||||||
|
so the document count is correct.
|
||||||
|
"""
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
lines = [line.rstrip("\n") for line in f if line.strip()]
|
||||||
|
|
||||||
|
if lines and lines[0].strip() == "List1":
|
||||||
|
cells = lines[1:]
|
||||||
|
rows = [cells[i:i + 3] for i in range(0, len(cells), 3)]
|
||||||
|
return [r for r in rows if len(r) == 3]
|
||||||
|
|
||||||
|
return [line.split("\t") for line in lines]
|
||||||
|
|
||||||
|
|
||||||
|
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 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 (FCCS
|
||||||
|
strips characters illegal in Windows filenames) don't cause false
|
||||||
|
mismatches.
|
||||||
|
"""
|
||||||
|
base = _PAGE_RE.sub("", name)
|
||||||
|
return re.sub(r"[^a-z0-9]+", "", base.lower())
|
||||||
|
|
||||||
|
|
||||||
|
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 (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 index_files_by_drawer(export_dir):
|
||||||
|
"""Map each export file to its leading drawer-ID token (before first '_').
|
||||||
|
|
||||||
|
The underscore boundary keeps clashing IDs separate (04289 vs 04289TS).
|
||||||
|
Returns {drawer_id: [filenames]}.
|
||||||
|
"""
|
||||||
|
index = {}
|
||||||
|
for f in os.listdir(export_dir):
|
||||||
|
if not os.path.isfile(os.path.join(export_dir, f)):
|
||||||
|
continue
|
||||||
|
token = f.split("_", 1)[0]
|
||||||
|
index.setdefault(token, []).append(f)
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_drawer(drawer_id, files, manifest_dir):
|
||||||
|
"""Compare a drawer's manifest against its exported files.
|
||||||
|
|
||||||
|
`files` is the list of export filenames belonging to this drawer. Accounts
|
||||||
|
for page-splitting (a document exported as 'Name Page 1/2/...' counts as
|
||||||
|
present) and filename sanitization (via match_key).
|
||||||
|
|
||||||
|
Returns a dict:
|
||||||
|
has_manifest, manifest_path, expected (list), missing (list),
|
||||||
|
extras (list), unparsed (int), file_count (int)
|
||||||
|
"""
|
||||||
|
manifest_path = os.path.join(manifest_dir, drawer_id + ".txt")
|
||||||
|
result = {
|
||||||
|
"has_manifest": os.path.exists(manifest_path),
|
||||||
|
"manifest_path": manifest_path,
|
||||||
|
"expected": [],
|
||||||
|
"missing": [],
|
||||||
|
"extras": [],
|
||||||
|
"unparsed": 0,
|
||||||
|
"file_count": len(files),
|
||||||
|
}
|
||||||
|
if not result["has_manifest"]:
|
||||||
|
return result
|
||||||
|
|
||||||
|
expected = manifest_doc_names(manifest_path, drawer_id)
|
||||||
|
result["expected"] = expected
|
||||||
|
|
||||||
|
# Group exported files by normalized doc name; page-splits collapse together.
|
||||||
|
exported = {} # key -> list of full doc names (one entry per file/page)
|
||||||
|
unparsed = 0
|
||||||
|
for f in files:
|
||||||
|
doc = exported_doc_name(f)
|
||||||
|
if doc is None:
|
||||||
|
unparsed += 1
|
||||||
|
continue
|
||||||
|
exported.setdefault(match_key(doc), []).append(doc)
|
||||||
|
|
||||||
|
result["missing"] = [name for name in expected
|
||||||
|
if match_key(name) not in exported]
|
||||||
|
|
||||||
|
expected_keys = {match_key(n) for n in expected}
|
||||||
|
result["extras"] = [names[0] for k, names in exported.items()
|
||||||
|
if k not in expected_keys]
|
||||||
|
result["unparsed"] = unparsed
|
||||||
|
return result
|
||||||
|
|||||||
124
fccs_verify.py
124
fccs_verify.py
@@ -1,35 +1,24 @@
|
|||||||
"""
|
"""
|
||||||
Step 4 (optional): Verify exported files against manifests.
|
Step 4 (optional): Verify exported files against manifests.
|
||||||
|
|
||||||
Compares the per-drawer manifests captured during export (Step 2)
|
Batch check across EVERY drawer that has a manifest: compares each drawer's
|
||||||
against the actual files in the export directory to identify
|
manifest (captured during Step 2) against the files in the export directory and
|
||||||
missing or extra files.
|
reports which drawers are complete vs missing documents.
|
||||||
|
|
||||||
|
Matching is document-level and identical to fccs_check.py (via
|
||||||
|
fccs_config.evaluate_drawer): it accounts for page-splitting (a document
|
||||||
|
exported as 'Name Page 1', 'Name Page 2', ... counts as present) and for
|
||||||
|
filename sanitization (titles with characters illegal in filenames still
|
||||||
|
match). Use fccs_check.py to spot-check individual drawers interactively.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from fccs_config import parse_args, load_config, make_logger
|
from fccs_config import (
|
||||||
|
parse_args, load_config, make_logger,
|
||||||
|
evaluate_drawer, index_files_by_drawer,
|
||||||
def load_manifest(path):
|
)
|
||||||
"""Load a manifest file and return one row per document.
|
|
||||||
|
|
||||||
Handles both formats:
|
|
||||||
- New: one document per line (tab-separated columns).
|
|
||||||
- Old: a raw ListView dump led by 'List1', then row-major cells with
|
|
||||||
3 columns per document (Drawer ID, Page Title, Application). We
|
|
||||||
reshape it so the document count is correct.
|
|
||||||
"""
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
lines = [line.rstrip("\n") for line in f if line.strip()]
|
|
||||||
|
|
||||||
if lines and lines[0].strip() == "List1":
|
|
||||||
cells = lines[1:]
|
|
||||||
rows = [cells[i:i + 3] for i in range(0, len(cells), 3)]
|
|
||||||
return [r for r in rows if len(r) == 3]
|
|
||||||
|
|
||||||
return [line.split("\t") for line in lines]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -49,77 +38,56 @@ def main():
|
|||||||
log(f"ERROR: export directory not found: {export_dir}")
|
log(f"ERROR: export directory not found: {export_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Load all manifests
|
drawer_ids = sorted(
|
||||||
manifests = {}
|
os.path.splitext(f)[0]
|
||||||
for fname in sorted(os.listdir(manifest_dir)):
|
for f in os.listdir(manifest_dir)
|
||||||
if fname.endswith(".txt"):
|
if f.endswith(".txt")
|
||||||
drawer_id = os.path.splitext(fname)[0]
|
)
|
||||||
items = load_manifest(os.path.join(manifest_dir, fname))
|
if not drawer_ids:
|
||||||
manifests[drawer_id] = items
|
|
||||||
|
|
||||||
if not manifests:
|
|
||||||
log("No manifest files found.")
|
log("No manifest files found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Index exported files by drawer ID prefix
|
files_by_drawer = index_files_by_drawer(export_dir)
|
||||||
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("=" * 60)
|
||||||
log("Export Verification Report")
|
log("Export Verification Report")
|
||||||
log("=" * 60)
|
log("=" * 60)
|
||||||
log(f"Manifests loaded : {len(manifests)} drawers")
|
log(f"Manifests loaded : {len(drawer_ids)} drawers")
|
||||||
log(f"Exported files : {len(all_exported)} total")
|
|
||||||
log("")
|
log("")
|
||||||
|
|
||||||
total_expected = 0
|
complete_ids = []
|
||||||
total_actual = 0
|
incomplete = [] # (drawer_id, missing_list)
|
||||||
mismatched = []
|
|
||||||
|
|
||||||
for drawer_id in sorted(manifests):
|
for drawer_id in drawer_ids:
|
||||||
expected = manifests[drawer_id]
|
r = evaluate_drawer(drawer_id, files_by_drawer.get(drawer_id, []),
|
||||||
actual = exported_by_drawer.get(drawer_id, [])
|
manifest_dir)
|
||||||
n_expected = len(expected)
|
n_expected = len(r["expected"])
|
||||||
n_actual = len(actual)
|
missing = r["missing"]
|
||||||
total_expected += n_expected
|
if not missing:
|
||||||
total_actual += n_actual
|
complete_ids.append(drawer_id)
|
||||||
|
log(f" {drawer_id}: OK ({n_expected} docs, {r['file_count']} files)")
|
||||||
if n_expected == n_actual:
|
|
||||||
log(f" {drawer_id}: OK ({n_actual} files)")
|
|
||||||
else:
|
else:
|
||||||
diff = n_actual - n_expected
|
incomplete.append((drawer_id, missing))
|
||||||
sign = "+" if diff > 0 else ""
|
log(f" {drawer_id}: INCOMPLETE — {len(missing)}/{n_expected} "
|
||||||
log(f" {drawer_id}: MISMATCH — expected {n_expected}, "
|
f"document(s) missing:")
|
||||||
f"got {n_actual} ({sign}{diff})")
|
for m in missing:
|
||||||
mismatched.append(drawer_id)
|
log(f" - {m}")
|
||||||
|
|
||||||
# Check for exported files with no manifest
|
# Exported files whose drawer has no manifest at all.
|
||||||
orphan_drawers = set(exported_by_drawer.keys()) - set(manifests.keys())
|
orphan_drawers = sorted(set(files_by_drawer) - set(drawer_ids))
|
||||||
orphan_count = sum(len(exported_by_drawer[d]) for d in orphan_drawers)
|
orphan_count = sum(len(files_by_drawer[d]) for d in orphan_drawers)
|
||||||
|
|
||||||
log("")
|
log("")
|
||||||
log("-" * 60)
|
log("-" * 60)
|
||||||
log(f"Expected total : {total_expected}")
|
log(f"Complete drawers : {len(complete_ids)}")
|
||||||
log(f"Actual total : {total_actual}")
|
log(f"Incomplete drawers : {len(incomplete)}")
|
||||||
|
if incomplete:
|
||||||
|
log(f" Incomplete IDs: {', '.join(d for d, _ in incomplete)}")
|
||||||
if orphan_drawers:
|
if orphan_drawers:
|
||||||
log(f"No manifest for : {', '.join(sorted(orphan_drawers))} "
|
log(f"No manifest for : {', '.join(orphan_drawers)} "
|
||||||
f"({orphan_count} files)")
|
f"({orphan_count} files)")
|
||||||
|
if not incomplete and not orphan_drawers:
|
||||||
if mismatched:
|
log("All drawers complete.")
|
||||||
log(f"Mismatched drawers: {', '.join(mismatched)}")
|
|
||||||
elif not orphan_drawers:
|
|
||||||
log("All drawers match.")
|
|
||||||
|
|
||||||
log("=" * 60)
|
log("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user