[{"data":1,"prerenderedAt":2525},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job":2491,"ancestors:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job":2499,"children:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job":2504,"related:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job":2505},{"id":4,"title":5,"body":6,"dateModified":2459,"datePublished":2459,"description":2460,"extension":2461,"faq":2462,"keyword":2471,"kind":2472,"meta":2473,"navigation":114,"path":2481,"seo":2482,"slug":2485,"stem":2486,"topics":2487,"type":2489,"__hash__":2490},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job\u002Findex.md","Quarantine Bad Excel Files in a Batch Job",{"type":7,"value":8,"toc":2448},"minimark",[9,29,34,62,79,83,86,435,456,598,602,605,1189,1223,1227,1797,1811,1941,1945,1948,2218,2233,2326,2330,2367,2371,2383,2387,2393,2399,2405,2411,2415,2444],[10,11,12,13,17,18,22,23,28],"p",{},"A job that processes every workbook dropped into a folder — branch returns, supplier price lists, timesheets — will sooner or later meet a file it cannot handle: one saved as CSV with an ",[14,15,16],"code",{},".xlsx"," name, one cut short by a failed upload, one where someone renamed a column, one full of text where numbers should be. The two common reactions are both bad. Letting the exception stop the job means one person's broken file delays everybody's data. Catching and logging it, then carrying on, means the bad file sits in the inbox and fails again on every run, and nobody tells its sender. The pattern that works is ",[19,20,21],"em",{},"quarantine",": move the bad file out of the way, next to a short note saying exactly what is wrong, and keep going. This page builds that for a folder of Excel files, with error classification, notes people can act on, a run summary and a way to resubmit fixed files. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,42,43],{"__ignoreMap":40},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," pandas",[44,60,61],{"class":54}," openpyxl\n",[10,63,64,65,68,69,72,73,75,76,78],{},"Everything else is the standard library. The sample inbox holds six branch returns: two good ones, one with a renamed column (",[19,66,67],{},"Qty"," instead of ",[19,70,71],{},"Units","), one with words in the ",[19,74,71],{}," column, one that is really a CSV file with an ",[14,77,16],{}," name, and one truncated half-way through — a cross-section of what turns up in real shared folders.",[30,80,82],{"id":81},"step-1-folders-for-each-state","Step 1 — Folders for each state",[10,84,85],{},"Give each state of a file its own folder on the same file system, so that moving a file between them is an atomic rename:",[35,87,91],{"className":88,"code":89,"language":90,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\n\nROOT = Path(\"drop\")\nINBOX, WORK, DONE, QUARANTINE = (ROOT \u002F n for n in (\"inbox\", \"processing\", \"done\", \"quarantine\"))\nfor d in (WORK, DONE, QUARANTINE):\n    d.mkdir(exist_ok=True)\n\ndef move(path, folder):\n    target = folder \u002F path.name\n    if target.exists():\n        target = folder \u002F f\"{path.stem}.{datetime.now():%Y%m%d%H%M%S}{path.suffix}\"\n    path.replace(target)\n    return target\n\nfrom datetime import datetime\nprint(sorted(p.name for p in INBOX.glob(\"*.xlsx\")))\n","python",[14,92,93,109,116,135,201,226,244,249,262,279,288,368,374,383,388,401],{"__ignoreMap":40},[44,94,95,99,103,106],{"class":46,"line":47},[44,96,98],{"class":97},"s-kum","from",[44,100,102],{"class":101},"skGVy"," pathlib ",[44,104,105],{"class":97},"import",[44,107,108],{"class":101}," Path\n",[44,110,112],{"class":46,"line":111},2,[44,113,115],{"emptyLinePlaceholder":114},true,"\n",[44,117,119,123,126,129,132],{"class":46,"line":118},3,[44,120,122],{"class":121},"sP0c6","ROOT",[44,124,125],{"class":97}," =",[44,127,128],{"class":101}," Path(",[44,130,131],{"class":54},"\"drop\"",[44,133,134],{"class":101},")\n",[44,136,138,141,144,147,149,152,154,157,159,162,164,167,170,173,175,178,180,183,185,188,190,193,195,198],{"class":46,"line":137},4,[44,139,140],{"class":121},"INBOX",[44,142,143],{"class":101},", ",[44,145,146],{"class":121},"WORK",[44,148,143],{"class":101},[44,150,151],{"class":121},"DONE",[44,153,143],{"class":101},[44,155,156],{"class":121},"QUARANTINE",[44,158,125],{"class":97},[44,160,161],{"class":101}," (",[44,163,122],{"class":121},[44,165,166],{"class":97}," \u002F",[44,168,169],{"class":101}," n ",[44,171,172],{"class":97},"for",[44,174,169],{"class":101},[44,176,177],{"class":97},"in",[44,179,161],{"class":101},[44,181,182],{"class":54},"\"inbox\"",[44,184,143],{"class":101},[44,186,187],{"class":54},"\"processing\"",[44,189,143],{"class":101},[44,191,192],{"class":54},"\"done\"",[44,194,143],{"class":101},[44,196,197],{"class":54},"\"quarantine\"",[44,199,200],{"class":101},"))\n",[44,202,204,206,209,211,213,215,217,219,221,223],{"class":46,"line":203},5,[44,205,172],{"class":97},[44,207,208],{"class":101}," d ",[44,210,177],{"class":97},[44,212,161],{"class":101},[44,214,146],{"class":121},[44,216,143],{"class":101},[44,218,151],{"class":121},[44,220,143],{"class":101},[44,222,156],{"class":121},[44,224,225],{"class":101},"):\n",[44,227,229,232,236,239,242],{"class":46,"line":228},6,[44,230,231],{"class":101},"    d.mkdir(",[44,233,235],{"class":234},"sa561","exist_ok",[44,237,238],{"class":97},"=",[44,240,241],{"class":121},"True",[44,243,134],{"class":101},[44,245,247],{"class":46,"line":246},7,[44,248,115],{"emptyLinePlaceholder":114},[44,250,252,255,259],{"class":46,"line":251},8,[44,253,254],{"class":97},"def",[44,256,258],{"class":257},"s_Opv"," move",[44,260,261],{"class":101},"(path, folder):\n",[44,263,265,268,270,273,276],{"class":46,"line":264},9,[44,266,267],{"class":101},"    target ",[44,269,238],{"class":97},[44,271,272],{"class":101}," folder ",[44,274,275],{"class":97},"\u002F",[44,277,278],{"class":101}," path.name\n",[44,280,282,285],{"class":46,"line":281},10,[44,283,284],{"class":97},"    if",[44,286,287],{"class":101}," target.exists():\n",[44,289,291,294,296,298,300,303,306,310,313,316,318,320,323,326,329,332,334,337,339,342,344,347,349,352,354,357,360,363,365],{"class":46,"line":290},11,[44,292,293],{"class":101},"        target ",[44,295,238],{"class":97},[44,297,272],{"class":101},[44,299,275],{"class":97},[44,301,302],{"class":97}," f",[44,304,305],{"class":54},"\"",[44,307,309],{"class":308},"sSjpA","{",[44,311,312],{"class":101},"path.stem",[44,314,315],{"class":308},"}",[44,317,28],{"class":54},[44,319,309],{"class":308},[44,321,322],{"class":101},"datetime.now()",[44,324,325],{"class":97},":",[44,327,328],{"class":101},"%",[44,330,331],{"class":97},"Y",[44,333,328],{"class":101},[44,335,336],{"class":97},"m",[44,338,328],{"class":101},[44,340,341],{"class":97},"d",[44,343,328],{"class":101},[44,345,346],{"class":97},"H",[44,348,328],{"class":101},[44,350,351],{"class":97},"M",[44,353,328],{"class":101},[44,355,356],{"class":97},"S",[44,358,359],{"class":308},"}{",[44,361,362],{"class":101},"path.suffix",[44,364,315],{"class":308},[44,366,367],{"class":54},"\"\n",[44,369,371],{"class":46,"line":370},12,[44,372,373],{"class":101},"    path.replace(target)\n",[44,375,377,380],{"class":46,"line":376},13,[44,378,379],{"class":97},"    return",[44,381,382],{"class":101}," target\n",[44,384,386],{"class":46,"line":385},14,[44,387,115],{"emptyLinePlaceholder":114},[44,389,391,393,396,398],{"class":46,"line":390},15,[44,392,98],{"class":97},[44,394,395],{"class":101}," datetime ",[44,397,105],{"class":97},[44,399,400],{"class":101}," datetime\n",[44,402,404,407,410,413,416,418,421,423,426,429,432],{"class":46,"line":403},16,[44,405,406],{"class":121},"print",[44,408,409],{"class":101},"(",[44,411,412],{"class":121},"sorted",[44,414,415],{"class":101},"(p.name ",[44,417,172],{"class":97},[44,419,420],{"class":101}," p ",[44,422,177],{"class":97},[44,424,425],{"class":121}," INBOX",[44,427,428],{"class":101},".glob(",[44,430,431],{"class":54},"\"*.xlsx\"",[44,433,434],{"class":101},")))\n",[10,436,437,440,441,444,445,447,448,451,452,28],{},[14,438,439],{},"Path.replace"," is an atomic rename when source and target are on the same file system, so a file is always in exactly one folder — never half-copied, never in two places. Moving each file into ",[19,442,443],{},"processing"," before touching it claims it: if a second copy of the job starts, it will not pick up the same file, and if the job crashes, the file left in ",[19,446,443],{}," shows exactly where. Files that collide with an earlier one of the same name get a timestamp rather than overwriting it. Skipping Excel's ",[14,449,450],{},"~$"," lock files when listing the inbox is covered in ",[24,453,455],{"href":454},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder\u002F","Ignore Excel Lock Files When Processing a Folder",[457,458,466,467,466,471,466,475,466,483,466,493,466,504,466,511,466,516,466,522,466,527,466,531,466,538,466,542,466,545,466,549,466,552,466,557,466,564,466,569,466,572,466,576,466,579,466,584,466,591,466,595],"svg",{"viewBox":459,"role":460,"ariaLabelledBy":461,"xmlns":464,"style":465},"4 0 752 125","img",[462,463],"qt-states-t","qt-states-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[468,469,470],"title",{"id":462},"The life of a file in the batch",[472,473,474],"desc",{"id":463},"A file arrives in the inbox, is moved to processing to claim it, then either moves to done after its data is loaded or to a dated quarantine folder with a note explaining the problem. A fixed file is resubmitted by moving it back to the inbox.",[476,477],"rect",{"x":478,"y":479,"width":480,"height":481,"fill":482},"4","0","752","125","#ffffff",[476,484],{"x":485,"y":486,"width":487,"height":488,"rx":489,"fill":490,"stroke":491,"style":492},"20.0","16.0","153.0","93.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[494,495,497],"g",{"transform":496},"translate(84.5,29.0) scale(1.000)",[498,499],"path",{"d":500,"fill":501,"stroke":502,"style":503},"M3 6h6l2 2h10v11H3z","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[505,506,510],"text",{"x":507,"y":508,"style":509},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","inbox",[505,512,515],{"x":507,"y":513,"style":514},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","files arrive",[46,517],{"x1":518,"y1":519,"x2":520,"y2":519,"stroke":521,"style":492},"178.0","62.5","200.0","var(--brand,#5b5cf0)",[523,524],"polygon",{"points":525,"fill":526},"204.0,62.5 195.0,67.5 195.0,57.5","#5b5cf0",[476,528],{"x":529,"y":486,"width":487,"height":488,"rx":489,"fill":530,"stroke":521,"style":492},"209.0","#f0f4ff",[494,532,534],{"transform":533},"translate(273.5,29.0) scale(1.000)",[498,535],{"d":536,"fill":501,"stroke":537,"style":503},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--brand-strong,#4338ca)",[505,539,443],{"x":540,"y":508,"style":541},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[505,543,544],{"x":540,"y":513,"style":514},"claimed by one run",[46,546],{"x1":547,"y1":519,"x2":548,"y2":519,"stroke":521,"style":492},"367.0","389.0",[523,550],{"points":551,"fill":526},"393.0,62.5 384.0,67.5 384.0,57.5",[476,553],{"x":554,"y":486,"width":487,"height":488,"rx":489,"fill":555,"stroke":556,"style":492},"398.0","#d9f4f1","var(--teal,#0f9488)",[494,558,560],{"transform":559},"translate(462.5,29.0) scale(1.000)",[498,561],{"d":562,"fill":501,"stroke":563,"style":503},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[505,565,568],{"x":566,"y":508,"style":567},"474.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","done",[505,570,571],{"x":566,"y":513,"style":514},"data loaded",[46,573],{"x1":574,"y1":519,"x2":575,"y2":519,"stroke":521,"style":492},"556.0","578.0",[523,577],{"points":578,"fill":526},"582.0,62.5 573.0,67.5 573.0,57.5",[476,580],{"x":581,"y":486,"width":487,"height":488,"rx":489,"fill":582,"stroke":583,"style":492},"587.0","#fdefd8","var(--gold,#b4740a)",[494,585,587],{"transform":586},"translate(651.5,29.0) scale(1.000)",[498,588],{"d":589,"fill":501,"stroke":590,"style":503},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[505,592,21],{"x":593,"y":508,"style":594},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[505,596,597],{"x":593,"y":513,"style":514},"with a JSON note",[30,599,601],{"id":600},"step-2-classify-what-can-go-wrong","Step 2 — Classify what can go wrong",[10,603,604],{},"The note in quarantine is only useful if it says something a person can act on. Translate exceptions into a few categories with plain-language messages, and raise your own for validation failures:",[35,606,608],{"className":88,"code":607,"language":90,"meta":40,"style":40},"import zipfile\nimport pandas as pd\n\nREQUIRED = [\"Branch\", \"Product\", \"Units\", \"Revenue\"]\n\nclass BadFile(Exception):\n    def __init__(self, kind, message):\n        super().__init__(message)\n        self.kind = kind\n\ndef load_return(path):\n    if not zipfile.is_zipfile(path):\n        head = path.read_bytes()[:64]\n        if b\",\" in head and head.isascii():\n            raise BadFile(\"wrong-format\", \"This is a CSV file saved with an .xlsx name. Open it in Excel and save as Excel Workbook.\")\n        raise BadFile(\"unreadable\", \"The file is damaged or incomplete — please upload it again.\")\n    try:\n        df = pd.read_excel(path, keep_default_na=False, na_values=[\"\"])\n    except (zipfile.BadZipFile, KeyError, ValueError) as e:\n        raise BadFile(\"unreadable\", f\"Excel could not read the file ({type(e).__name__}) — please upload it again.\")\n    missing = [c for c in REQUIRED if c not in df.columns]\n    if missing:\n        raise BadFile(\"columns\", f\"Missing column(s) {', '.join(missing)}. Found: {', '.join(map(str, df.columns))}.\")\n    units = pd.to_numeric(df[\"Units\"], errors=\"coerce\")\n    bad = df.loc[units.isna() & df[\"Units\"].notna(), \"Units\"]\n    if len(bad):\n        rows = \", \".join(f\"row {i + 2} ('{v}')\" for i, v in bad.items())\n        raise BadFile(\"values\", f\"Units must be a number: {rows}.\")\n    return df.assign(Units=units, source=path.name)\n",[14,609,610,617,630,634,665,669,684,695,709,722,726,736,746,761,784,802,819,828,863,888,923,957,965,1020,1046,1072,1083,1138,1166],{"__ignoreMap":40},[44,611,612,614],{"class":46,"line":47},[44,613,105],{"class":97},[44,615,616],{"class":101}," zipfile\n",[44,618,619,621,624,627],{"class":46,"line":111},[44,620,105],{"class":97},[44,622,623],{"class":101}," pandas ",[44,625,626],{"class":97},"as",[44,628,629],{"class":101}," pd\n",[44,631,632],{"class":46,"line":118},[44,633,115],{"emptyLinePlaceholder":114},[44,635,636,639,641,644,647,649,652,654,657,659,662],{"class":46,"line":137},[44,637,638],{"class":121},"REQUIRED",[44,640,125],{"class":97},[44,642,643],{"class":101}," [",[44,645,646],{"class":54},"\"Branch\"",[44,648,143],{"class":101},[44,650,651],{"class":54},"\"Product\"",[44,653,143],{"class":101},[44,655,656],{"class":54},"\"Units\"",[44,658,143],{"class":101},[44,660,661],{"class":54},"\"Revenue\"",[44,663,664],{"class":101},"]\n",[44,666,667],{"class":46,"line":203},[44,668,115],{"emptyLinePlaceholder":114},[44,670,671,674,677,679,682],{"class":46,"line":228},[44,672,673],{"class":97},"class",[44,675,676],{"class":50}," BadFile",[44,678,409],{"class":101},[44,680,681],{"class":121},"Exception",[44,683,225],{"class":101},[44,685,686,689,692],{"class":46,"line":246},[44,687,688],{"class":97},"    def",[44,690,691],{"class":121}," __init__",[44,693,694],{"class":101},"(self, kind, message):\n",[44,696,697,700,703,706],{"class":46,"line":251},[44,698,699],{"class":121},"        super",[44,701,702],{"class":101},"().",[44,704,705],{"class":121},"__init__",[44,707,708],{"class":101},"(message)\n",[44,710,711,714,717,719],{"class":46,"line":264},[44,712,713],{"class":121},"        self",[44,715,716],{"class":101},".kind ",[44,718,238],{"class":97},[44,720,721],{"class":101}," kind\n",[44,723,724],{"class":46,"line":281},[44,725,115],{"emptyLinePlaceholder":114},[44,727,728,730,733],{"class":46,"line":290},[44,729,254],{"class":97},[44,731,732],{"class":257}," load_return",[44,734,735],{"class":101},"(path):\n",[44,737,738,740,743],{"class":46,"line":370},[44,739,284],{"class":97},[44,741,742],{"class":97}," not",[44,744,745],{"class":101}," zipfile.is_zipfile(path):\n",[44,747,748,751,753,756,759],{"class":46,"line":376},[44,749,750],{"class":101},"        head ",[44,752,238],{"class":97},[44,754,755],{"class":101}," path.read_bytes()[:",[44,757,758],{"class":121},"64",[44,760,664],{"class":101},[44,762,763,766,769,772,775,778,781],{"class":46,"line":385},[44,764,765],{"class":97},"        if",[44,767,768],{"class":97}," b",[44,770,771],{"class":54},"\",\"",[44,773,774],{"class":97}," in",[44,776,777],{"class":101}," head ",[44,779,780],{"class":97},"and",[44,782,783],{"class":101}," head.isascii():\n",[44,785,786,789,792,795,797,800],{"class":46,"line":390},[44,787,788],{"class":97},"            raise",[44,790,791],{"class":101}," BadFile(",[44,793,794],{"class":54},"\"wrong-format\"",[44,796,143],{"class":101},[44,798,799],{"class":54},"\"This is a CSV file saved with an .xlsx name. Open it in Excel and save as Excel Workbook.\"",[44,801,134],{"class":101},[44,803,804,807,809,812,814,817],{"class":46,"line":403},[44,805,806],{"class":97},"        raise",[44,808,791],{"class":101},[44,810,811],{"class":54},"\"unreadable\"",[44,813,143],{"class":101},[44,815,816],{"class":54},"\"The file is damaged or incomplete — please upload it again.\"",[44,818,134],{"class":101},[44,820,822,825],{"class":46,"line":821},17,[44,823,824],{"class":97},"    try",[44,826,827],{"class":101},":\n",[44,829,831,834,836,839,842,844,847,849,852,854,857,860],{"class":46,"line":830},18,[44,832,833],{"class":101},"        df ",[44,835,238],{"class":97},[44,837,838],{"class":101}," pd.read_excel(path, ",[44,840,841],{"class":234},"keep_default_na",[44,843,238],{"class":97},[44,845,846],{"class":121},"False",[44,848,143],{"class":101},[44,850,851],{"class":234},"na_values",[44,853,238],{"class":97},[44,855,856],{"class":101},"[",[44,858,859],{"class":54},"\"\"",[44,861,862],{"class":101},"])\n",[44,864,866,869,872,875,877,880,883,885],{"class":46,"line":865},19,[44,867,868],{"class":97},"    except",[44,870,871],{"class":101}," (zipfile.BadZipFile, ",[44,873,874],{"class":121},"KeyError",[44,876,143],{"class":101},[44,878,879],{"class":121},"ValueError",[44,881,882],{"class":101},") ",[44,884,626],{"class":97},[44,886,887],{"class":101}," e:\n",[44,889,891,893,895,897,899,902,905,907,910,913,916,918,921],{"class":46,"line":890},20,[44,892,806],{"class":97},[44,894,791],{"class":101},[44,896,811],{"class":54},[44,898,143],{"class":101},[44,900,901],{"class":97},"f",[44,903,904],{"class":54},"\"Excel could not read the file (",[44,906,309],{"class":308},[44,908,909],{"class":121},"type",[44,911,912],{"class":101},"(e).",[44,914,915],{"class":121},"__name__",[44,917,315],{"class":308},[44,919,920],{"class":54},") — please upload it again.\"",[44,922,134],{"class":101},[44,924,926,929,931,934,936,939,941,944,947,949,952,954],{"class":46,"line":925},21,[44,927,928],{"class":101},"    missing ",[44,930,238],{"class":97},[44,932,933],{"class":101}," [c ",[44,935,172],{"class":97},[44,937,938],{"class":101}," c ",[44,940,177],{"class":97},[44,942,943],{"class":121}," REQUIRED",[44,945,946],{"class":97}," if",[44,948,938],{"class":101},[44,950,951],{"class":97},"not",[44,953,774],{"class":97},[44,955,956],{"class":101}," df.columns]\n",[44,958,960,962],{"class":46,"line":959},22,[44,961,284],{"class":97},[44,963,964],{"class":101}," missing:\n",[44,966,968,970,972,975,977,979,982,984,987,990,992,995,997,999,1002,1005,1007,1010,1013,1015,1018],{"class":46,"line":967},23,[44,969,806],{"class":97},[44,971,791],{"class":101},[44,973,974],{"class":54},"\"columns\"",[44,976,143],{"class":101},[44,978,901],{"class":97},[44,980,981],{"class":54},"\"Missing column(s) ",[44,983,309],{"class":308},[44,985,986],{"class":54},"', '",[44,988,989],{"class":101},".join(missing)",[44,991,315],{"class":308},[44,993,994],{"class":54},". Found: ",[44,996,309],{"class":308},[44,998,986],{"class":54},[44,1000,1001],{"class":101},".join(",[44,1003,1004],{"class":121},"map",[44,1006,409],{"class":101},[44,1008,1009],{"class":121},"str",[44,1011,1012],{"class":101},", df.columns))",[44,1014,315],{"class":308},[44,1016,1017],{"class":54},".\"",[44,1019,134],{"class":101},[44,1021,1023,1026,1028,1031,1033,1036,1039,1041,1044],{"class":46,"line":1022},24,[44,1024,1025],{"class":101},"    units ",[44,1027,238],{"class":97},[44,1029,1030],{"class":101}," pd.to_numeric(df[",[44,1032,656],{"class":54},[44,1034,1035],{"class":101},"], ",[44,1037,1038],{"class":234},"errors",[44,1040,238],{"class":97},[44,1042,1043],{"class":54},"\"coerce\"",[44,1045,134],{"class":101},[44,1047,1049,1052,1054,1057,1060,1063,1065,1068,1070],{"class":46,"line":1048},25,[44,1050,1051],{"class":101},"    bad ",[44,1053,238],{"class":97},[44,1055,1056],{"class":101}," df.loc[units.isna() ",[44,1058,1059],{"class":97},"&",[44,1061,1062],{"class":101}," df[",[44,1064,656],{"class":54},[44,1066,1067],{"class":101},"].notna(), ",[44,1069,656],{"class":54},[44,1071,664],{"class":101},[44,1073,1075,1077,1080],{"class":46,"line":1074},26,[44,1076,284],{"class":97},[44,1078,1079],{"class":121}," len",[44,1081,1082],{"class":101},"(bad):\n",[44,1084,1086,1089,1091,1094,1096,1098,1101,1103,1106,1109,1112,1114,1117,1119,1122,1124,1127,1130,1133,1135],{"class":46,"line":1085},27,[44,1087,1088],{"class":101},"        rows ",[44,1090,238],{"class":97},[44,1092,1093],{"class":54}," \", \"",[44,1095,1001],{"class":101},[44,1097,901],{"class":97},[44,1099,1100],{"class":54},"\"row ",[44,1102,309],{"class":308},[44,1104,1105],{"class":101},"i ",[44,1107,1108],{"class":97},"+",[44,1110,1111],{"class":121}," 2",[44,1113,315],{"class":308},[44,1115,1116],{"class":54}," ('",[44,1118,309],{"class":308},[44,1120,1121],{"class":101},"v",[44,1123,315],{"class":308},[44,1125,1126],{"class":54},"')\"",[44,1128,1129],{"class":97}," for",[44,1131,1132],{"class":101}," i, v ",[44,1134,177],{"class":97},[44,1136,1137],{"class":101}," bad.items())\n",[44,1139,1141,1143,1145,1148,1150,1152,1155,1157,1160,1162,1164],{"class":46,"line":1140},28,[44,1142,806],{"class":97},[44,1144,791],{"class":101},[44,1146,1147],{"class":54},"\"values\"",[44,1149,143],{"class":101},[44,1151,901],{"class":97},[44,1153,1154],{"class":54},"\"Units must be a number: ",[44,1156,309],{"class":308},[44,1158,1159],{"class":101},"rows",[44,1161,315],{"class":308},[44,1163,1017],{"class":54},[44,1165,134],{"class":101},[44,1167,1169,1171,1174,1176,1178,1181,1184,1186],{"class":46,"line":1168},29,[44,1170,379],{"class":97},[44,1172,1173],{"class":101}," df.assign(",[44,1175,71],{"class":234},[44,1177,238],{"class":97},[44,1179,1180],{"class":101},"units, ",[44,1182,1183],{"class":234},"source",[44,1185,238],{"class":97},[44,1187,1188],{"class":101},"path.name)\n",[10,1190,1191,1192,1194,1195,1198,1199,1202,1203,68,1205,1207,1208,1211,1212,1214,1215,1218,1219,1222],{},"A truncated ",[14,1193,16],{}," is no longer a valid zip archive, which ",[14,1196,1197],{},"zipfile.is_zipfile"," detects before pandas produces a more obscure error; a CSV in disguise is recognised by its plain-text first bytes. Missing columns are reported together with the columns that ",[19,1200,1201],{},"were"," found, which usually makes the cause obvious — here, ",[19,1204,67],{},[19,1206,71],{},". Reading with ",[14,1209,1210],{},"keep_default_na=False"," and only ",[14,1213,859],{}," as a missing value matters for validation: by default pandas silently turns text such as \"n\u002Fa\", \"NA\" or \"null\" into missing values, which would let them slip past the numeric check as if the cells were simply empty. Value problems name the rows as Excel numbers them (data row ",[14,1216,1217],{},"i"," is sheet row ",[14,1220,1221],{},"i + 2",", after the header), so the sender can go straight to the cell. Any exception not anticipated here is still caught in the next step and quarantined as \"unexpected\", with its type and message.",[30,1224,1226],{"id":1225},"step-3-process-every-file-quarantine-failures-keep-going","Step 3 — Process every file, quarantine failures, keep going",[35,1228,1230],{"className":88,"code":1229,"language":90,"meta":40,"style":40},"import json\nimport traceback\nimport hashlib\n\ndef quarantine(path, kind, message, exc=None):\n    folder = QUARANTINE \u002F datetime.now().strftime(\"%Y-%m-%d\")\n    folder.mkdir(exist_ok=True)\n    target = move(path, folder)\n    note = {\"file\": path.name, \"kind\": kind, \"message\": message, \"quarantined_at\": datetime.now().isoformat(timespec=\"seconds\"),\n            \"sha256\": hashlib.sha256(target.read_bytes()).hexdigest()}\n    if exc is not None and kind == \"unexpected\":\n        note[\"detail\"] = traceback.format_exception_only(exc)[-1].strip()\n    target.with_name(target.name + \".error.json\").write_text(json.dumps(note, indent=2))\n    return note\n\ndef run_batch():\n    loaded, failed = [], []\n    for path in sorted(INBOX.glob(\"*.xlsx\")):\n        claimed = move(path, WORK)\n        try:\n            df = load_return(claimed)\n        except BadFile as e:\n            failed.append(quarantine(claimed, e.kind, str(e)))\n        except Exception as e:\n            failed.append(quarantine(claimed, \"unexpected\", \"The file could not be processed; the team has been told.\", e))\n        else:\n            loaded.append(df)\n            move(claimed, DONE)\n    combined = pd.concat(loaded, ignore_index=True) if loaded else pd.DataFrame(columns=REQUIRED)\n    return combined, failed\n\ncombined, failed = run_batch()\nprint(f\"loaded {combined['source'].nunique()} files, {len(combined)} rows; quarantined {len(failed)}\")\nfor note in failed:\n    print(f\"  {note['file']:\u003C20} {note['kind']:\u003C13} {note['message']}\")\n",[14,1231,1232,1239,1246,1253,1257,1274,1299,1312,1321,1366,1374,1403,1428,1451,1458,1462,1472,1482,1506,1520,1527,1537,1549,1559,1571,1587,1594,1599,1608,1648,1656,1661,1672,1726,1739],{"__ignoreMap":40},[44,1233,1234,1236],{"class":46,"line":47},[44,1235,105],{"class":97},[44,1237,1238],{"class":101}," json\n",[44,1240,1241,1243],{"class":46,"line":111},[44,1242,105],{"class":97},[44,1244,1245],{"class":101}," traceback\n",[44,1247,1248,1250],{"class":46,"line":118},[44,1249,105],{"class":97},[44,1251,1252],{"class":101}," hashlib\n",[44,1254,1255],{"class":46,"line":137},[44,1256,115],{"emptyLinePlaceholder":114},[44,1258,1259,1261,1264,1267,1269,1272],{"class":46,"line":203},[44,1260,254],{"class":97},[44,1262,1263],{"class":257}," quarantine",[44,1265,1266],{"class":101},"(path, kind, message, exc",[44,1268,238],{"class":97},[44,1270,1271],{"class":121},"None",[44,1273,225],{"class":101},[44,1275,1276,1279,1281,1284,1286,1289,1292,1295,1297],{"class":46,"line":228},[44,1277,1278],{"class":101},"    folder ",[44,1280,238],{"class":97},[44,1282,1283],{"class":121}," QUARANTINE",[44,1285,166],{"class":97},[44,1287,1288],{"class":101}," datetime.now().strftime(",[44,1290,1291],{"class":54},"\"%Y-%m-",[44,1293,1294],{"class":308},"%d",[44,1296,305],{"class":54},[44,1298,134],{"class":101},[44,1300,1301,1304,1306,1308,1310],{"class":46,"line":246},[44,1302,1303],{"class":101},"    folder.mkdir(",[44,1305,235],{"class":234},[44,1307,238],{"class":97},[44,1309,241],{"class":121},[44,1311,134],{"class":101},[44,1313,1314,1316,1318],{"class":46,"line":251},[44,1315,267],{"class":101},[44,1317,238],{"class":97},[44,1319,1320],{"class":101}," move(path, folder)\n",[44,1322,1323,1326,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1360,1363],{"class":46,"line":264},[44,1324,1325],{"class":101},"    note ",[44,1327,238],{"class":97},[44,1329,1330],{"class":101}," {",[44,1332,1333],{"class":54},"\"file\"",[44,1335,1336],{"class":101},": path.name, ",[44,1338,1339],{"class":54},"\"kind\"",[44,1341,1342],{"class":101},": kind, ",[44,1344,1345],{"class":54},"\"message\"",[44,1347,1348],{"class":101},": message, ",[44,1350,1351],{"class":54},"\"quarantined_at\"",[44,1353,1354],{"class":101},": datetime.now().isoformat(",[44,1356,1357],{"class":234},"timespec",[44,1359,238],{"class":97},[44,1361,1362],{"class":54},"\"seconds\"",[44,1364,1365],{"class":101},"),\n",[44,1367,1368,1371],{"class":46,"line":281},[44,1369,1370],{"class":54},"            \"sha256\"",[44,1372,1373],{"class":101},": hashlib.sha256(target.read_bytes()).hexdigest()}\n",[44,1375,1376,1378,1381,1384,1386,1389,1392,1395,1398,1401],{"class":46,"line":290},[44,1377,284],{"class":97},[44,1379,1380],{"class":101}," exc ",[44,1382,1383],{"class":97},"is",[44,1385,742],{"class":97},[44,1387,1388],{"class":121}," None",[44,1390,1391],{"class":97}," and",[44,1393,1394],{"class":101}," kind ",[44,1396,1397],{"class":97},"==",[44,1399,1400],{"class":54}," \"unexpected\"",[44,1402,827],{"class":101},[44,1404,1405,1408,1411,1414,1416,1419,1422,1425],{"class":46,"line":370},[44,1406,1407],{"class":101},"        note[",[44,1409,1410],{"class":54},"\"detail\"",[44,1412,1413],{"class":101},"] ",[44,1415,238],{"class":97},[44,1417,1418],{"class":101}," traceback.format_exception_only(exc)[",[44,1420,1421],{"class":97},"-",[44,1423,1424],{"class":121},"1",[44,1426,1427],{"class":101},"].strip()\n",[44,1429,1430,1433,1435,1438,1441,1444,1446,1449],{"class":46,"line":376},[44,1431,1432],{"class":101},"    target.with_name(target.name ",[44,1434,1108],{"class":97},[44,1436,1437],{"class":54}," \".error.json\"",[44,1439,1440],{"class":101},").write_text(json.dumps(note, ",[44,1442,1443],{"class":234},"indent",[44,1445,238],{"class":97},[44,1447,1448],{"class":121},"2",[44,1450,200],{"class":101},[44,1452,1453,1455],{"class":46,"line":385},[44,1454,379],{"class":97},[44,1456,1457],{"class":101}," note\n",[44,1459,1460],{"class":46,"line":390},[44,1461,115],{"emptyLinePlaceholder":114},[44,1463,1464,1466,1469],{"class":46,"line":403},[44,1465,254],{"class":97},[44,1467,1468],{"class":257}," run_batch",[44,1470,1471],{"class":101},"():\n",[44,1473,1474,1477,1479],{"class":46,"line":821},[44,1475,1476],{"class":101},"    loaded, failed ",[44,1478,238],{"class":97},[44,1480,1481],{"class":101}," [], []\n",[44,1483,1484,1487,1490,1492,1495,1497,1499,1501,1503],{"class":46,"line":830},[44,1485,1486],{"class":97},"    for",[44,1488,1489],{"class":101}," path ",[44,1491,177],{"class":97},[44,1493,1494],{"class":121}," sorted",[44,1496,409],{"class":101},[44,1498,140],{"class":121},[44,1500,428],{"class":101},[44,1502,431],{"class":54},[44,1504,1505],{"class":101},")):\n",[44,1507,1508,1511,1513,1516,1518],{"class":46,"line":865},[44,1509,1510],{"class":101},"        claimed ",[44,1512,238],{"class":97},[44,1514,1515],{"class":101}," move(path, ",[44,1517,146],{"class":121},[44,1519,134],{"class":101},[44,1521,1522,1525],{"class":46,"line":890},[44,1523,1524],{"class":97},"        try",[44,1526,827],{"class":101},[44,1528,1529,1532,1534],{"class":46,"line":925},[44,1530,1531],{"class":101},"            df ",[44,1533,238],{"class":97},[44,1535,1536],{"class":101}," load_return(claimed)\n",[44,1538,1539,1542,1545,1547],{"class":46,"line":959},[44,1540,1541],{"class":97},"        except",[44,1543,1544],{"class":101}," BadFile ",[44,1546,626],{"class":97},[44,1548,887],{"class":101},[44,1550,1551,1554,1556],{"class":46,"line":967},[44,1552,1553],{"class":101},"            failed.append(quarantine(claimed, e.kind, ",[44,1555,1009],{"class":121},[44,1557,1558],{"class":101},"(e)))\n",[44,1560,1561,1563,1566,1569],{"class":46,"line":1022},[44,1562,1541],{"class":97},[44,1564,1565],{"class":121}," Exception",[44,1567,1568],{"class":97}," as",[44,1570,887],{"class":101},[44,1572,1573,1576,1579,1581,1584],{"class":46,"line":1048},[44,1574,1575],{"class":101},"            failed.append(quarantine(claimed, ",[44,1577,1578],{"class":54},"\"unexpected\"",[44,1580,143],{"class":101},[44,1582,1583],{"class":54},"\"The file could not be processed; the team has been told.\"",[44,1585,1586],{"class":101},", e))\n",[44,1588,1589,1592],{"class":46,"line":1074},[44,1590,1591],{"class":97},"        else",[44,1593,827],{"class":101},[44,1595,1596],{"class":46,"line":1085},[44,1597,1598],{"class":101},"            loaded.append(df)\n",[44,1600,1601,1604,1606],{"class":46,"line":1140},[44,1602,1603],{"class":101},"            move(claimed, ",[44,1605,151],{"class":121},[44,1607,134],{"class":101},[44,1609,1610,1613,1615,1618,1621,1623,1625,1627,1630,1633,1636,1639,1642,1644,1646],{"class":46,"line":1168},[44,1611,1612],{"class":101},"    combined ",[44,1614,238],{"class":97},[44,1616,1617],{"class":101}," pd.concat(loaded, ",[44,1619,1620],{"class":234},"ignore_index",[44,1622,238],{"class":97},[44,1624,241],{"class":121},[44,1626,882],{"class":101},[44,1628,1629],{"class":97},"if",[44,1631,1632],{"class":101}," loaded ",[44,1634,1635],{"class":97},"else",[44,1637,1638],{"class":101}," pd.DataFrame(",[44,1640,1641],{"class":234},"columns",[44,1643,238],{"class":97},[44,1645,638],{"class":121},[44,1647,134],{"class":101},[44,1649,1651,1653],{"class":46,"line":1650},30,[44,1652,379],{"class":97},[44,1654,1655],{"class":101}," combined, failed\n",[44,1657,1659],{"class":46,"line":1658},31,[44,1660,115],{"emptyLinePlaceholder":114},[44,1662,1664,1667,1669],{"class":46,"line":1663},32,[44,1665,1666],{"class":101},"combined, failed ",[44,1668,238],{"class":97},[44,1670,1671],{"class":101}," run_batch()\n",[44,1673,1675,1677,1679,1681,1684,1686,1689,1692,1695,1697,1700,1702,1705,1708,1710,1713,1715,1717,1720,1722,1724],{"class":46,"line":1674},33,[44,1676,406],{"class":121},[44,1678,409],{"class":101},[44,1680,901],{"class":97},[44,1682,1683],{"class":54},"\"loaded ",[44,1685,309],{"class":308},[44,1687,1688],{"class":101},"combined[",[44,1690,1691],{"class":54},"'source'",[44,1693,1694],{"class":101},"].nunique()",[44,1696,315],{"class":308},[44,1698,1699],{"class":54}," files, ",[44,1701,309],{"class":308},[44,1703,1704],{"class":121},"len",[44,1706,1707],{"class":101},"(combined)",[44,1709,315],{"class":308},[44,1711,1712],{"class":54}," rows; quarantined ",[44,1714,309],{"class":308},[44,1716,1704],{"class":121},[44,1718,1719],{"class":101},"(failed)",[44,1721,315],{"class":308},[44,1723,305],{"class":54},[44,1725,134],{"class":101},[44,1727,1729,1731,1734,1736],{"class":46,"line":1728},34,[44,1730,172],{"class":97},[44,1732,1733],{"class":101}," note ",[44,1735,177],{"class":97},[44,1737,1738],{"class":101}," failed:\n",[44,1740,1742,1745,1747,1749,1752,1754,1757,1760,1763,1766,1768,1770,1772,1775,1777,1780,1782,1784,1786,1789,1791,1793,1795],{"class":46,"line":1741},35,[44,1743,1744],{"class":121},"    print",[44,1746,409],{"class":101},[44,1748,901],{"class":97},[44,1750,1751],{"class":54},"\"  ",[44,1753,309],{"class":308},[44,1755,1756],{"class":101},"note[",[44,1758,1759],{"class":54},"'file'",[44,1761,1762],{"class":101},"]",[44,1764,1765],{"class":97},":\u003C20",[44,1767,315],{"class":308},[44,1769,1330],{"class":308},[44,1771,1756],{"class":101},[44,1773,1774],{"class":54},"'kind'",[44,1776,1762],{"class":101},[44,1778,1779],{"class":97},":\u003C13",[44,1781,315],{"class":308},[44,1783,1330],{"class":308},[44,1785,1756],{"class":101},[44,1787,1788],{"class":54},"'message'",[44,1790,1762],{"class":101},[44,1792,315],{"class":308},[44,1794,305],{"class":54},[44,1796,134],{"class":101},[10,1798,1799,1800,1803,1804,1806,1807,1810],{},"Each file is processed inside its own ",[14,1801,1802],{},"try",", so a failure affects only that file. The ",[14,1805,1635],{}," branch runs only when loading succeeded, which keeps the \"move to done\" step out of the error handling. Two files load and four are quarantined, each with a message the branch could act on: re-upload, save as a workbook, rename the column, fix rows 2 and 4 — the second being the \"n\u002Fa\" that default parsing would have hidden. The note sits beside the file as ",[14,1808,1809],{},"name.xlsx.error.json",", with a hash so you can tell whether a resubmitted file is actually different from the one that failed.",[457,1812,466,1817,466,1820,466,1823,466,1826,466,1831,466,1836,466,1842,466,1845,466,1849,466,1853,466,1859,466,1861,466,1865,466,1867,466,1870,466,1873,466,1877,466,1879,466,1883,466,1885,466,1888,466,1891,466,1894,466,1896,466,1900,466,1902,466,1905,466,1908,466,1912,466,1914,466,1918,466,1920,466,1923,466,1926,466,1930,466,1932,466,1936,466,1938],{"viewBox":1813,"role":460,"ariaLabelledBy":1814,"xmlns":464,"style":465},"4 0 752 242",[1815,1816],"qt-kinds-t","qt-kinds-d",[468,1818,1819],{"id":1815},"Failure kinds and what the sender should do",[472,1821,1822],{"id":1816},"An unreadable file should be uploaded again. A CSV with an xlsx name should be saved as an Excel workbook. Missing columns should be renamed to the expected names. Non-numeric values should be corrected in the listed rows. Unexpected errors go to the team that runs the job.",[476,1824],{"x":478,"y":479,"width":480,"height":1825,"fill":482},"242",[476,1827],{"x":485,"y":486,"width":1828,"height":1829,"rx":479,"fill":482,"stroke":491,"style":1830},"118.2","40.0","stroke-width:1px",[476,1832],{"x":1833,"y":486,"width":1834,"height":1829,"rx":479,"fill":530,"stroke":521,"style":1835},"138.2","300.9","stroke-width:1.2px",[505,1837,1841],{"x":1838,"y":1839,"style":1840},"288.6","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","example",[476,1843],{"x":1844,"y":486,"width":1834,"height":1829,"rx":479,"fill":530,"stroke":521,"style":1835},"439.1",[505,1846,1848],{"x":1847,"y":1839,"style":1840},"589.5","sender should",[476,1850],{"x":485,"y":1851,"width":1828,"height":1852,"rx":479,"fill":490,"stroke":491,"style":1830},"56.0","34.0",[505,1854,1858],{"x":1855,"y":1856,"style":1857},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","unreadable",[476,1860],{"x":1833,"y":1851,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1862,1864],{"x":1838,"y":1863,"style":514},"77.0","truncated upload",[476,1866],{"x":1844,"y":1851,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1868,1869],{"x":1847,"y":1863,"style":514},"upload again",[476,1871],{"x":485,"y":1872,"width":1828,"height":1852,"rx":479,"fill":490,"stroke":491,"style":1830},"90.0",[505,1874,1876],{"x":1855,"y":1875,"style":1857},"112.0","wrong-format",[476,1878],{"x":1833,"y":1872,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1880,1882],{"x":1838,"y":1881,"style":514},"111.0","CSV named .xlsx",[476,1884],{"x":1844,"y":1872,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1886,1887],{"x":1847,"y":1881,"style":514},"save as workbook",[476,1889],{"x":485,"y":1890,"width":1828,"height":1852,"rx":479,"fill":490,"stroke":491,"style":1830},"124.0",[505,1892,1641],{"x":1855,"y":1893,"style":1857},"146.0",[476,1895],{"x":1833,"y":1890,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1897,1899],{"x":1838,"y":1898,"style":514},"145.0","Qty, not Units",[476,1901],{"x":1844,"y":1890,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1903,1904],{"x":1847,"y":1898,"style":514},"rename the column",[476,1906],{"x":485,"y":1907,"width":1828,"height":1852,"rx":479,"fill":490,"stroke":491,"style":1830},"158.0",[505,1909,1911],{"x":1855,"y":1910,"style":1857},"180.0","values",[476,1913],{"x":1833,"y":1907,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1915,1917],{"x":1838,"y":1916,"style":514},"179.0","'ten' in Units",[476,1919],{"x":1844,"y":1907,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1921,1922],{"x":1847,"y":1916,"style":514},"fix the listed rows",[476,1924],{"x":485,"y":1925,"width":1828,"height":1852,"rx":479,"fill":490,"stroke":491,"style":1830},"192.0",[505,1927,1929],{"x":1855,"y":1928,"style":1857},"214.0","unexpected",[476,1931],{"x":1833,"y":1925,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1933,1935],{"x":1838,"y":1934,"style":514},"213.0","a bug",[476,1937],{"x":1844,"y":1925,"width":1834,"height":1852,"rx":479,"fill":482,"stroke":491,"style":1830},[505,1939,1940],{"x":1847,"y":1934,"style":514},"nothing — team fixes",[30,1942,1944],{"id":1943},"step-4-report-the-run-and-resubmit-fixed-files","Step 4 — Report the run and resubmit fixed files",[10,1946,1947],{},"A quarantined file that nobody hears about is only slightly better than a lost one. Summarise every run, and make resubmission as simple as moving the fixed file back to the inbox:",[35,1949,1951],{"className":88,"code":1950,"language":90,"meta":40,"style":40},"def summary(failed):\n    lines = [f\"{len(failed)} file(s) need attention:\"]\n    lines += [f\"- {n['file']}: {n['message']}\" for n in failed]\n    return \"\\n\".join(lines)\n\nprint(summary(failed))\n\nfixed = QUARANTINE \u002F datetime.now().strftime(\"%Y-%m-%d\") \u002F \"hull_2026-09.xlsx\"\ndf = pd.read_excel(fixed).rename(columns={\"Qty\": \"Units\"})\ndf.to_excel(INBOX \u002F \"hull_2026-09.xlsx\", index=False)\nfixed.unlink(); fixed.with_name(fixed.name + \".error.json\").unlink()\ncombined, failed = run_batch()\nprint(\"rerun loaded:\", combined[\"source\"].unique().tolist(), \"| quarantined:\", len(failed))\nprint(\"done folder:\", sorted(p.name for p in DONE.iterdir()))\n",[14,1952,1953,1963,1989,2038,2053,2057,2064,2068,2094,2120,2143,2155,2163,2191],{"__ignoreMap":40},[44,1954,1955,1957,1960],{"class":46,"line":47},[44,1956,254],{"class":97},[44,1958,1959],{"class":257}," summary",[44,1961,1962],{"class":101},"(failed):\n",[44,1964,1965,1968,1970,1972,1974,1976,1978,1980,1982,1984,1987],{"class":46,"line":111},[44,1966,1967],{"class":101},"    lines ",[44,1969,238],{"class":97},[44,1971,643],{"class":101},[44,1973,901],{"class":97},[44,1975,305],{"class":54},[44,1977,309],{"class":308},[44,1979,1704],{"class":121},[44,1981,1719],{"class":101},[44,1983,315],{"class":308},[44,1985,1986],{"class":54}," file(s) need attention:\"",[44,1988,664],{"class":101},[44,1990,1991,1993,1996,1998,2000,2003,2005,2008,2010,2012,2014,2017,2019,2021,2023,2025,2027,2029,2031,2033,2035],{"class":46,"line":118},[44,1992,1967],{"class":101},[44,1994,1995],{"class":97},"+=",[44,1997,643],{"class":101},[44,1999,901],{"class":97},[44,2001,2002],{"class":54},"\"- ",[44,2004,309],{"class":308},[44,2006,2007],{"class":101},"n[",[44,2009,1759],{"class":54},[44,2011,1762],{"class":101},[44,2013,315],{"class":308},[44,2015,2016],{"class":54},": ",[44,2018,309],{"class":308},[44,2020,2007],{"class":101},[44,2022,1788],{"class":54},[44,2024,1762],{"class":101},[44,2026,315],{"class":308},[44,2028,305],{"class":54},[44,2030,1129],{"class":97},[44,2032,169],{"class":101},[44,2034,177],{"class":97},[44,2036,2037],{"class":101}," failed]\n",[44,2039,2040,2042,2045,2048,2050],{"class":46,"line":137},[44,2041,379],{"class":97},[44,2043,2044],{"class":54}," \"",[44,2046,2047],{"class":308},"\\n",[44,2049,305],{"class":54},[44,2051,2052],{"class":101},".join(lines)\n",[44,2054,2055],{"class":46,"line":203},[44,2056,115],{"emptyLinePlaceholder":114},[44,2058,2059,2061],{"class":46,"line":228},[44,2060,406],{"class":121},[44,2062,2063],{"class":101},"(summary(failed))\n",[44,2065,2066],{"class":46,"line":246},[44,2067,115],{"emptyLinePlaceholder":114},[44,2069,2070,2073,2075,2077,2079,2081,2083,2085,2087,2089,2091],{"class":46,"line":251},[44,2071,2072],{"class":101},"fixed ",[44,2074,238],{"class":97},[44,2076,1283],{"class":121},[44,2078,166],{"class":97},[44,2080,1288],{"class":101},[44,2082,1291],{"class":54},[44,2084,1294],{"class":308},[44,2086,305],{"class":54},[44,2088,882],{"class":101},[44,2090,275],{"class":97},[44,2092,2093],{"class":54}," \"hull_2026-09.xlsx\"\n",[44,2095,2096,2099,2101,2104,2106,2108,2110,2113,2115,2117],{"class":46,"line":264},[44,2097,2098],{"class":101},"df ",[44,2100,238],{"class":97},[44,2102,2103],{"class":101}," pd.read_excel(fixed).rename(",[44,2105,1641],{"class":234},[44,2107,238],{"class":97},[44,2109,309],{"class":101},[44,2111,2112],{"class":54},"\"Qty\"",[44,2114,2016],{"class":101},[44,2116,656],{"class":54},[44,2118,2119],{"class":101},"})\n",[44,2121,2122,2125,2127,2129,2132,2134,2137,2139,2141],{"class":46,"line":281},[44,2123,2124],{"class":101},"df.to_excel(",[44,2126,140],{"class":121},[44,2128,166],{"class":97},[44,2130,2131],{"class":54}," \"hull_2026-09.xlsx\"",[44,2133,143],{"class":101},[44,2135,2136],{"class":234},"index",[44,2138,238],{"class":97},[44,2140,846],{"class":121},[44,2142,134],{"class":101},[44,2144,2145,2148,2150,2152],{"class":46,"line":290},[44,2146,2147],{"class":101},"fixed.unlink(); fixed.with_name(fixed.name ",[44,2149,1108],{"class":97},[44,2151,1437],{"class":54},[44,2153,2154],{"class":101},").unlink()\n",[44,2156,2157,2159,2161],{"class":46,"line":370},[44,2158,1666],{"class":101},[44,2160,238],{"class":97},[44,2162,1671],{"class":101},[44,2164,2165,2167,2169,2172,2175,2178,2181,2184,2186,2188],{"class":46,"line":376},[44,2166,406],{"class":121},[44,2168,409],{"class":101},[44,2170,2171],{"class":54},"\"rerun loaded:\"",[44,2173,2174],{"class":101},", combined[",[44,2176,2177],{"class":54},"\"source\"",[44,2179,2180],{"class":101},"].unique().tolist(), ",[44,2182,2183],{"class":54},"\"| quarantined:\"",[44,2185,143],{"class":101},[44,2187,1704],{"class":121},[44,2189,2190],{"class":101},"(failed))\n",[44,2192,2193,2195,2197,2200,2202,2204,2206,2208,2210,2212,2215],{"class":46,"line":385},[44,2194,406],{"class":121},[44,2196,409],{"class":101},[44,2198,2199],{"class":54},"\"done folder:\"",[44,2201,143],{"class":101},[44,2203,412],{"class":121},[44,2205,415],{"class":101},[44,2207,172],{"class":97},[44,2209,420],{"class":101},[44,2211,177],{"class":97},[44,2213,2214],{"class":121}," DONE",[44,2216,2217],{"class":101},".iterdir()))\n",[10,2219,2220,2221,2223,2224,2228,2229,28],{},"The summary is ready to email to the people who submit files or post to a channel — grouping by sender, if the file names or a lookup tell you who sent what, turns it into a personal to-do list for each branch. After the Hull return is fixed and put back in the inbox, the next run loads it and the ",[19,2222,568],{}," folder now holds three returns. Because loaded files leave the inbox, rerunning the job never loads the same file twice. Sending the summary is covered in ",[24,2225,2227],{"href":2226},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fsend-an-alert-when-an-excel-report-job-fails\u002F","Send an Alert When an Excel Report Job Fails","; untrusted uploads deserve the extra precautions in ",[24,2230,2232],{"href":2231},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python\u002F","Read Untrusted Excel Files Safely in Python",[457,2234,466,2239,466,2242,466,2245,466,2248,466,2255,466,2259,466,2265,466,2270,466,2274,466,2278,466,2282,466,2288,466,2292,466,2294,466,2296,466,2299,466,2304,466,2307,466,2309,466,2311,466,2314,466,2319,466,2322,466,2324],{"viewBox":2235,"role":460,"ariaLabelledBy":2236,"xmlns":464,"style":465},"4 0 752 211",[2237,2238],"qt-resubmit-t","qt-resubmit-d",[468,2240,2241],{"id":2237},"A rejected return on its way back in",[472,2243,2244],{"id":2238},"On the first run the Hull return is quarantined because its Units column is called Qty. The branch is told in the run summary, fixes the header and moves the file back to the inbox. The next run loads it and moves it to done.",[476,2246],{"x":478,"y":479,"width":480,"height":2247,"fill":482},"211",[46,2249],{"x1":2250,"y1":2251,"x2":2252,"y2":2251,"stroke":2253,"style":2254},"94.0","105.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[476,2256],{"x":485,"y":486,"width":2257,"height":2258,"rx":489,"fill":582,"stroke":583,"style":492},"172.0","57.5",[505,2260,2264],{"x":2261,"y":2262,"style":2263},"106.0","40.5","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Run 1",[505,2266,2269],{"x":2261,"y":2267,"style":2268},"59.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","quarantined: missing Units",[46,2271],{"x1":2261,"y1":2272,"x2":2261,"y2":2251,"stroke":2253,"style":2273},"73.5","stroke-width:1.5px",[2275,2276],"circle",{"cx":2261,"cy":2251,"r":2277,"fill":583,"stroke":482,"style":492},"7.0",[476,2279],{"x":2280,"y":2281,"width":2257,"height":2258,"rx":489,"fill":490,"stroke":491,"style":492},"202.7","137.5",[505,2283,2287],{"x":2284,"y":2285,"style":2286},"288.7","162.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Summary sent",[505,2289,2291],{"x":2284,"y":2290,"style":2268},"180.5","rename Qty to Units",[46,2293],{"x1":2284,"y1":2251,"x2":2284,"y2":2281,"stroke":2253,"style":2273},[2275,2295],{"cx":2284,"cy":2251,"r":2277,"fill":2253,"stroke":482,"style":492},[476,2297],{"x":2298,"y":486,"width":2257,"height":2258,"rx":489,"fill":530,"stroke":521,"style":492},"385.3",[505,2300,2303],{"x":2301,"y":2262,"style":2302},"471.3","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Resubmitted",[505,2305,2306],{"x":2301,"y":2267,"style":2268},"moved back to inbox",[46,2308],{"x1":2301,"y1":2272,"x2":2301,"y2":2251,"stroke":2253,"style":2273},[2275,2310],{"cx":2301,"cy":2251,"r":2277,"fill":521,"stroke":482,"style":492},[476,2312],{"x":2313,"y":2281,"width":2257,"height":2258,"rx":489,"fill":555,"stroke":556,"style":492},"568.0",[505,2315,2318],{"x":2316,"y":2285,"style":2317},"654.0","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Run 2",[505,2320,2321],{"x":2316,"y":2290,"style":2268},"loaded, moved to done",[46,2323],{"x1":2316,"y1":2251,"x2":2316,"y2":2281,"stroke":2253,"style":2273},[2275,2325],{"cx":2316,"cy":2251,"r":2277,"fill":556,"stroke":482,"style":492},[30,2327,2329],{"id":2328},"common-pitfalls","Common pitfalls",[2331,2332,2333,2343,2349,2355,2361],"ul",{},[2334,2335,2336,2340,2341,28],"li",{},[2337,2338,2339],"strong",{},"Letting one file stop the batch."," Wrap each file in its own ",[14,2342,1802],{},[2334,2344,2345,2348],{},[2337,2346,2347],{},"Logging and leaving the file in place."," It fails on every run; move it.",[2334,2350,2351,2354],{},[2337,2352,2353],{},"Messages only developers understand."," Say what the sender should do.",[2334,2356,2357,2360],{},[2337,2358,2359],{},"Copy then delete."," Use an atomic rename within one file system.",[2334,2362,2363,2366],{},[2337,2364,2365],{},"No summary."," Tell the people who can fix the files.",[30,2368,2370],{"id":2369},"conclusion","Conclusion",[10,2372,2373,2374,2376,2377,2379,2380,2382],{},"Process a folder of workbooks one file at a time: claim each by moving it to a ",[19,2375,443],{}," folder, load and validate it, and move it to ",[19,2378,568],{}," or to a dated ",[19,2381,21],{}," folder with a JSON note that classifies the problem and tells the sender what to do. Catch unexpected errors too, so nothing stops the batch, summarise every run for the people who can act, and let them resubmit by putting the fixed file back in the inbox.",[30,2384,2386],{"id":2385},"frequently-asked-questions","Frequently asked questions",[10,2388,2389,2392],{},[2337,2390,2391],{},"How do I stop one bad Excel file from crashing a batch job?","\nProcess each file inside its own try block, and on failure move the file to a quarantine folder with a note instead of letting the exception end the job.",[10,2394,2395,2398],{},[2337,2396,2397],{},"How do I detect a corrupt or truncated xlsx file?","\nAn xlsx is a zip archive, so zipfile.is_zipfile returns False for truncated files and for CSV or text files given an xlsx name.",[10,2400,2401,2404],{},[2337,2402,2403],{},"Where should a quarantined file's error message go?","\nIn a small JSON file next to it with the failure kind, a plain-language message and a hash of the file, and in a run summary sent to the people who submit files.",[10,2406,2407,2410],{},[2337,2408,2409],{},"How do I resubmit a file after fixing it?","\nMove the corrected file back into the inbox folder; the next run picks it up like any new file.",[30,2412,2414],{"id":2413},"related","Related",[2331,2416,2417,2423,2430,2437],{},[2334,2418,2419,2420,2422],{},"Up one level: ",[24,2421,27],{"href":26}," — reliable Excel jobs.",[2334,2424,2425,2429],{},[24,2426,2428],{"href":2427},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," — checks on the output side.",[2334,2431,2432,2436],{},[24,2433,2435],{"href":2434},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun\u002F","Make an Excel Report Job Safe to Rerun"," — reruns without side effects.",[2334,2438,2439,2443],{},[24,2440,2442],{"href":2441},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files\u002F","Watch a Folder and Process New Excel Files with Python"," — triggering the batch when files arrive.",[2445,2446,2447],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":40,"searchDepth":111,"depth":111,"links":2449},[2450,2451,2452,2453,2454,2455,2456,2457,2458],{"id":32,"depth":111,"text":33},{"id":81,"depth":111,"text":82},{"id":600,"depth":111,"text":601},{"id":1225,"depth":111,"text":1226},{"id":1943,"depth":111,"text":1944},{"id":2328,"depth":111,"text":2329},{"id":2369,"depth":111,"text":2370},{"id":2385,"depth":111,"text":2386},{"id":2413,"depth":111,"text":2414},"2026-10-02","Keep one broken workbook from stopping a batch job that processes a folder of Excel files: claim each file, process it in isolation, move failures to a dated quarantine folder with a JSON note explaining what went wrong, keep going with the rest, summarise the run, and let people fix and resubmit files by moving them back.","md",[2463,2465,2467,2469],{"q":2391,"a":2464},"Process each file inside its own try block, and on failure move the file to a quarantine folder with a note instead of letting the exception end the job.",{"q":2397,"a":2466},"An xlsx is a zip archive, so zipfile.is_zipfile returns False for truncated files and for CSV or text files given an xlsx name.",{"q":2403,"a":2468},"In a small JSON file next to it with the failure kind, a plain-language message and a hash of the file, and in a run summary sent to the people who submit files.",{"q":2409,"a":2470},"Move the corrected file back into the inbox folder; the next run picks it up like any new file.","quarantine bad excel files in a batch job",null,{"breadcrumb":2474},[2475,2477,2480],{"name":2476,"item":275},"Home",{"name":2478,"item":2479},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":27,"item":26},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job",{"title":2483,"description":2484},"Quarantine Bad Excel Files in a Python Batch Job","Folder-based batch processing of Excel files that survives bad input: inbox, processing, done and quarantine folders, error classification, JSON sidecar notes, atomic moves, run summaries and resubmission.","quarantine-bad-excel-files-in-a-batch-job","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job\u002Findex",[2488],"reliable-jobs","how-to","B2X4N-wzv6Q8vDAS9Tkhwswb0SPApmgYtjHwEB-PVwg",[2492,2496],{"title":2493,"path":2494,"stem":2495,"children":-1},"Prevent Overlapping Runs of an Excel Report Job","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002Findex",{"title":2232,"path":2497,"stem":2498,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python\u002Findex",[2500,2502],{"path":2501,"title":27},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation",{"path":2503,"title":2478},"\u002Fautomating-reporting-workflows",[],[2506,2510,2513,2516,2519,2522],{"path":2507,"title":2508,"kind":2509},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","Testing and Packaging Excel Automation Scripts","guide",{"path":2511,"title":2512,"kind":2489},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fblock-large-or-sensitive-excel-files-with-a-pre-commit-hook","Block Large or Sensitive Excel Files with a Pre-commit Hook",{"path":2514,"title":2515,"kind":2489},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse","Build a Command-Line Tool for Excel Reports with argparse",{"path":2517,"title":2518,"kind":2489},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests","Compare Excel Output to a Golden File in Tests",{"path":2520,"title":2521,"kind":2489},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","Create Excel Test Fixtures in Memory for pytest",{"path":2523,"title":2524,"kind":2489},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver","Diff Excel Files in Git with a textconv Driver",1790972132099]