[{"data":1,"prerenderedAt":3164},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python":3155},{"id":4,"title":5,"body":6,"dateModified":3130,"datePublished":3130,"description":3131,"extension":3132,"faq":3133,"meta":3146,"navigation":208,"path":3147,"seo":3148,"slug":3151,"stem":3152,"type":3153,"__hash__":3154},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python\u002Findex.md","Write Excel Files to a Network Share from Python",{"type":7,"value":8,"toc":3118},"minimark",[9,19,144,149,177,180,297,312,316,323,646,668,671,901,905,912,1017,1403,1410,1413,1666,1677,1681,1684,1970,1977,1981,2111,2114,2520,2527,2531,2668,2672,2679,2690,2798,2808,2900,2906,2916,3009,3013,3022,3026,3032,3041,3055,3061,3073,3077,3114],[10,11,12,13,18],"p",{},"Not every organisation publishes to the cloud. A network share is still where a great many recurring reports land — it is already there, everyone has a drive letter for it, and no security review is required. The pitfalls are different from an API upload but no less real: a reader can open the file in Excel and lock it, a slow copy leaves a truncated workbook visible for the whole transfer, and a script that works from your terminal fails under the scheduler because the drive letter does not exist there. This guide covers writing to a share safely. It is the on-premise path from ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",".",[20,21,30,31,30,35,30,39,30,46,30,56,30,63,30,68,30,73,30,79,30,84,30,91,30,96,30,101,30,106,30,110,30,114,30,118,30,124,30,128,30,132,30,135,30,138],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 236","img","Atomic publish to a share: write to a temporary name in the destination folder, then rename it over the target so readers never observe a partial file.",[26,27],"share-t","share-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"Write-then-rename on a network share",[36,37,38],"desc",{"id":27},"The report is written to a temporary file inside the destination folder, so the transfer happens under a name nobody is looking at. When the write completes, os.replace renames it over the target in one filesystem operation. A reader opening the folder during the transfer sees only the previous version; after the rename they see only the complete new one. Writing directly to the target instead would expose a growing partial file for the whole copy.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","236","#ffffff",[40,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"14","84","160","70","13","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,62],"text",{"x":59,"y":60,"style":61},"94","68","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the job",[57,64,67],{"x":59,"y":65,"style":66},"114","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","report.xlsx",[57,69,72],{"x":59,"y":70,"style":71},"135","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","built locally",[74,75],"line",{"x1":76,"y1":77,"x2":78,"y2":77,"stroke":54,"style":55},"174","119","206",[80,81],"polygon",{"points":82,"fill":83},"214,119 202,113 202,125","#5b5cf0",[40,85],{"x":86,"y":87,"width":88,"height":59,"rx":52,"fill":89,"stroke":90,"style":55},"222","72","216","#fdefd8","var(--gold,#b4740a)",[57,92,95],{"x":93,"y":94,"style":61},"330","56","on the share, same folder",[57,97,100],{"x":93,"y":98,"style":99},"102","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","report.xlsx.tmp7f2a",[57,102,105],{"x":93,"y":103,"style":104},"126","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","the slow copy happens here",[57,107,109],{"x":93,"y":108,"style":71},"146","nobody is looking at this name",[74,111],{"x1":112,"y1":77,"x2":113,"y2":77,"stroke":90,"style":55},"438","470",[80,115],{"points":116,"fill":117},"478,119 466,113 466,125","#b4740a",[40,119],{"x":120,"y":87,"width":121,"height":59,"rx":52,"fill":122,"stroke":123,"style":55},"486","298","#d9f4f1","var(--teal,#0f9488)",[57,125,127],{"x":126,"y":94,"style":61},"635","one atomic operation",[57,129,131],{"x":126,"y":98,"style":130},"font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","os.replace(tmp, target)",[57,133,134],{"x":126,"y":103,"style":104},"readers see the old file, then the new",[57,136,137],{"x":126,"y":108,"style":71},"never a partial one",[57,139,143],{"x":140,"y":141,"style":142},"400","204","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the temporary file must be on the SAME filesystem, or the rename becomes a copy",[145,146,148],"h2",{"id":147},"prerequisites","Prerequisites",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter\n","bash","",[157,158,159],"code",{"__ignoreMap":155},[160,161,163,167,171,174],"span",{"class":74,"line":162},1,[160,164,166],{"class":165},"sMTad","pip",[160,168,170],{"class":169},"srMev"," install",[160,172,173],{"class":169}," pandas",[160,175,176],{"class":169}," xlsxwriter\n",[10,178,179],{},"Access to the share, addressed the right way for the platform:",[150,181,185],{"className":182,"code":183,"language":184,"meta":155,"style":155},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\n\n# Windows: a UNC path, not a mapped drive letter.\nshare = Path(r\"\\\\fileserver\\reports\\monthly\")\n\n# Linux\u002FmacOS: mount the share first, then treat it as an ordinary path.\n# sudo mount -t cifs \u002F\u002Ffileserver\u002Freports \u002Fmnt\u002Freports \\\n#     -o credentials=\u002Fetc\u002Fsamba\u002Freport-creds,uid=1000\nshare = Path(\"\u002Fmnt\u002Freports\u002Fmonthly\")\n","python",[157,186,187,203,210,217,257,262,268,277,283],{"__ignoreMap":155},[160,188,189,193,197,200],{"class":74,"line":162},[160,190,192],{"class":191},"s-kum","from",[160,194,196],{"class":195},"skGVy"," pathlib ",[160,198,199],{"class":191},"import",[160,201,202],{"class":195}," Path\n",[160,204,206],{"class":74,"line":205},2,[160,207,209],{"emptyLinePlaceholder":208},true,"\n",[160,211,213],{"class":74,"line":212},3,[160,214,216],{"class":215},"s-wDw","# Windows: a UNC path, not a mapped drive letter.\n",[160,218,220,223,226,229,232,235,239,242,245,248,251,254],{"class":74,"line":219},4,[160,221,222],{"class":195},"share ",[160,224,225],{"class":191},"=",[160,227,228],{"class":195}," Path(",[160,230,231],{"class":191},"r",[160,233,234],{"class":169},"\"",[160,236,238],{"class":237},"s_b0D","\\\\",[160,240,241],{"class":169},"fileserver",[160,243,244],{"class":237},"\\r",[160,246,247],{"class":169},"eports",[160,249,250],{"class":237},"\\m",[160,252,253],{"class":169},"onthly\"",[160,255,256],{"class":195},")\n",[160,258,260],{"class":74,"line":259},5,[160,261,209],{"emptyLinePlaceholder":208},[160,263,265],{"class":74,"line":264},6,[160,266,267],{"class":215},"# Linux\u002FmacOS: mount the share first, then treat it as an ordinary path.\n",[160,269,271,274],{"class":74,"line":270},7,[160,272,273],{"class":215},"# sudo mount -t cifs",[160,275,276],{"class":215}," \u002F\u002Ffileserver\u002Freports \u002Fmnt\u002Freports \\\n",[160,278,280],{"class":74,"line":279},8,[160,281,282],{"class":215},"#     -o credentials=\u002Fetc\u002Fsamba\u002Freport-creds,uid=1000\n",[160,284,286,288,290,292,295],{"class":74,"line":285},9,[160,287,222],{"class":195},[160,289,225],{"class":191},[160,291,228],{"class":195},[160,293,294],{"class":169},"\"\u002Fmnt\u002Freports\u002Fmonthly\"",[160,296,256],{"class":195},[10,298,299,303,304,307,308,18],{},[300,301,302],"strong",{},"Use the UNC path, not a drive letter."," Mapped drives are per-session: ",[157,305,306],{},"R:\\"," exists in your interactive session and does not exist for the account a scheduled task runs under. This single detail accounts for most \"works for me, fails under the scheduler\" reports — the fuller treatment of scheduler environments is in ",[14,309,311],{"href":310},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002F","running a Python Excel script on Windows Task Scheduler",[145,313,315],{"id":314},"step-1-write-atomically","Step 1 — Write atomically",[10,317,318,319,322],{},"Write to a temporary name in the destination folder, then rename. ",[157,320,321],{},"os.replace"," is a single filesystem operation on both Windows and POSIX, so a reader sees the old file or the new one and nothing in between:",[150,324,326],{"className":182,"code":325,"language":184,"meta":155,"style":155},"import os\nimport tempfile\nfrom pathlib import Path\n\ndef publish_atomic(local_path, share_dir, name):\n    \"\"\"Copy a report onto a share without ever exposing a partial file.\"\"\"\n    share_dir = Path(share_dir)\n    share_dir.mkdir(parents=True, exist_ok=True)\n    target = share_dir \u002F name\n\n    # The temporary file MUST live in the destination folder, or the rename\n    # crosses filesystems and degrades into a non-atomic copy.\n    fd, tmp_name = tempfile.mkstemp(dir=share_dir, prefix=\".\", suffix=\".tmp\")\n    tmp = Path(tmp_name)\n    try:\n        with os.fdopen(fd, \"wb\") as out, open(local_path, \"rb\") as src:\n            while chunk := src.read(1 \u003C\u003C 20):\n                out.write(chunk)\n            out.flush()\n            os.fsync(out.fileno())      # force the bytes out before renaming\n\n        os.replace(tmp, target)         # atomic within one filesystem\n    except BaseException:\n        tmp.unlink(missing_ok=True)     # never leave litter behind\n        raise\n    return target\n",[157,327,328,335,342,352,356,368,373,383,410,426,431,437,443,482,493,502,539,566,572,578,587,592,601,612,631,637],{"__ignoreMap":155},[160,329,330,332],{"class":74,"line":162},[160,331,199],{"class":191},[160,333,334],{"class":195}," os\n",[160,336,337,339],{"class":74,"line":205},[160,338,199],{"class":191},[160,340,341],{"class":195}," tempfile\n",[160,343,344,346,348,350],{"class":74,"line":212},[160,345,192],{"class":191},[160,347,196],{"class":195},[160,349,199],{"class":191},[160,351,202],{"class":195},[160,353,354],{"class":74,"line":219},[160,355,209],{"emptyLinePlaceholder":208},[160,357,358,361,365],{"class":74,"line":259},[160,359,360],{"class":191},"def",[160,362,364],{"class":363},"s_Opv"," publish_atomic",[160,366,367],{"class":195},"(local_path, share_dir, name):\n",[160,369,370],{"class":74,"line":264},[160,371,372],{"class":169},"    \"\"\"Copy a report onto a share without ever exposing a partial file.\"\"\"\n",[160,374,375,378,380],{"class":74,"line":270},[160,376,377],{"class":195},"    share_dir ",[160,379,225],{"class":191},[160,381,382],{"class":195}," Path(share_dir)\n",[160,384,385,388,392,394,398,401,404,406,408],{"class":74,"line":279},[160,386,387],{"class":195},"    share_dir.mkdir(",[160,389,391],{"class":390},"sa561","parents",[160,393,225],{"class":191},[160,395,397],{"class":396},"sP0c6","True",[160,399,400],{"class":195},", ",[160,402,403],{"class":390},"exist_ok",[160,405,225],{"class":191},[160,407,397],{"class":396},[160,409,256],{"class":195},[160,411,412,415,417,420,423],{"class":74,"line":285},[160,413,414],{"class":195},"    target ",[160,416,225],{"class":191},[160,418,419],{"class":195}," share_dir ",[160,421,422],{"class":191},"\u002F",[160,424,425],{"class":195}," name\n",[160,427,429],{"class":74,"line":428},10,[160,430,209],{"emptyLinePlaceholder":208},[160,432,434],{"class":74,"line":433},11,[160,435,436],{"class":215},"    # The temporary file MUST live in the destination folder, or the rename\n",[160,438,440],{"class":74,"line":439},12,[160,441,442],{"class":215},"    # crosses filesystems and degrades into a non-atomic copy.\n",[160,444,446,449,451,454,457,459,462,465,467,470,472,475,477,480],{"class":74,"line":445},13,[160,447,448],{"class":195},"    fd, tmp_name ",[160,450,225],{"class":191},[160,452,453],{"class":195}," tempfile.mkstemp(",[160,455,456],{"class":390},"dir",[160,458,225],{"class":191},[160,460,461],{"class":195},"share_dir, ",[160,463,464],{"class":390},"prefix",[160,466,225],{"class":191},[160,468,469],{"class":169},"\".\"",[160,471,400],{"class":195},[160,473,474],{"class":390},"suffix",[160,476,225],{"class":191},[160,478,479],{"class":169},"\".tmp\"",[160,481,256],{"class":195},[160,483,485,488,490],{"class":74,"line":484},14,[160,486,487],{"class":195},"    tmp ",[160,489,225],{"class":191},[160,491,492],{"class":195}," Path(tmp_name)\n",[160,494,496,499],{"class":74,"line":495},15,[160,497,498],{"class":191},"    try",[160,500,501],{"class":195},":\n",[160,503,505,508,511,514,517,520,523,526,529,532,534,536],{"class":74,"line":504},16,[160,506,507],{"class":191},"        with",[160,509,510],{"class":195}," os.fdopen(fd, ",[160,512,513],{"class":169},"\"wb\"",[160,515,516],{"class":195},") ",[160,518,519],{"class":191},"as",[160,521,522],{"class":195}," out, ",[160,524,525],{"class":396},"open",[160,527,528],{"class":195},"(local_path, ",[160,530,531],{"class":169},"\"rb\"",[160,533,516],{"class":195},[160,535,519],{"class":191},[160,537,538],{"class":195}," src:\n",[160,540,542,545,548,551,554,557,560,563],{"class":74,"line":541},17,[160,543,544],{"class":191},"            while",[160,546,547],{"class":195}," chunk ",[160,549,550],{"class":191},":=",[160,552,553],{"class":195}," src.read(",[160,555,556],{"class":396},"1",[160,558,559],{"class":191}," \u003C\u003C",[160,561,562],{"class":396}," 20",[160,564,565],{"class":195},"):\n",[160,567,569],{"class":74,"line":568},18,[160,570,571],{"class":195},"                out.write(chunk)\n",[160,573,575],{"class":74,"line":574},19,[160,576,577],{"class":195},"            out.flush()\n",[160,579,581,584],{"class":74,"line":580},20,[160,582,583],{"class":195},"            os.fsync(out.fileno())      ",[160,585,586],{"class":215},"# force the bytes out before renaming\n",[160,588,590],{"class":74,"line":589},21,[160,591,209],{"emptyLinePlaceholder":208},[160,593,595,598],{"class":74,"line":594},22,[160,596,597],{"class":195},"        os.replace(tmp, target)         ",[160,599,600],{"class":215},"# atomic within one filesystem\n",[160,602,604,607,610],{"class":74,"line":603},23,[160,605,606],{"class":191},"    except",[160,608,609],{"class":396}," BaseException",[160,611,501],{"class":195},[160,613,615,618,621,623,625,628],{"class":74,"line":614},24,[160,616,617],{"class":195},"        tmp.unlink(",[160,619,620],{"class":390},"missing_ok",[160,622,225],{"class":191},[160,624,397],{"class":396},[160,626,627],{"class":195},")     ",[160,629,630],{"class":215},"# never leave litter behind\n",[160,632,634],{"class":74,"line":633},25,[160,635,636],{"class":191},"        raise\n",[160,638,640,643],{"class":74,"line":639},26,[160,641,642],{"class":191},"    return",[160,644,645],{"class":195}," target\n",[10,647,648,649,652,653,655,656,659,660,663,664,667],{},"Three details earn their place. The ",[157,650,651],{},"dir=share_dir"," argument keeps the temporary file on the same volume — put it in the system temp directory instead and ",[157,654,321],{}," falls back to a copy, losing the guarantee entirely. The ",[157,657,658],{},"os.fsync"," forces buffered data to the server before the rename, so a crash between write and rename cannot promote a partial file. And the ",[157,661,662],{},"except"," clause removes the temporary file on any failure, including a keyboard interrupt, so a folder does not silently fill with ",[157,665,666],{},".tmpXXXX"," fragments.",[10,669,670],{},"Writing the workbook straight to the share is the same shape:",[150,672,674],{"className":182,"code":673,"language":184,"meta":155,"style":155},"import io\nimport pandas as pd\n\ndef publish_dataframe(df, share_dir, name, sheet_name=\"Summary\"):\n    buffer = io.BytesIO()\n    with pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, sheet_name=sheet_name, index=False)\n\n    share_dir = Path(share_dir)\n    fd, tmp_name = tempfile.mkstemp(dir=share_dir, prefix=\".\", suffix=\".tmp\")\n    tmp = Path(tmp_name)\n    try:\n        with os.fdopen(fd, \"wb\") as out:\n            out.write(buffer.getvalue())\n            out.flush()\n            os.fsync(out.fileno())\n        os.replace(tmp, share_dir \u002F name)\n    except BaseException:\n        tmp.unlink(missing_ok=True)\n        raise\n    return share_dir \u002F name\n",[157,675,676,683,695,699,716,726,749,772,776,784,814,822,828,843,848,852,857,867,875,887,891],{"__ignoreMap":155},[160,677,678,680],{"class":74,"line":162},[160,679,199],{"class":191},[160,681,682],{"class":195}," io\n",[160,684,685,687,690,692],{"class":74,"line":205},[160,686,199],{"class":191},[160,688,689],{"class":195}," pandas ",[160,691,519],{"class":191},[160,693,694],{"class":195}," pd\n",[160,696,697],{"class":74,"line":212},[160,698,209],{"emptyLinePlaceholder":208},[160,700,701,703,706,709,711,714],{"class":74,"line":219},[160,702,360],{"class":191},[160,704,705],{"class":363}," publish_dataframe",[160,707,708],{"class":195},"(df, share_dir, name, sheet_name",[160,710,225],{"class":191},[160,712,713],{"class":169},"\"Summary\"",[160,715,565],{"class":195},[160,717,718,721,723],{"class":74,"line":259},[160,719,720],{"class":195},"    buffer ",[160,722,225],{"class":191},[160,724,725],{"class":195}," io.BytesIO()\n",[160,727,728,731,734,737,739,742,744,746],{"class":74,"line":264},[160,729,730],{"class":191},"    with",[160,732,733],{"class":195}," pd.ExcelWriter(buffer, ",[160,735,736],{"class":390},"engine",[160,738,225],{"class":191},[160,740,741],{"class":169},"\"xlsxwriter\"",[160,743,516],{"class":195},[160,745,519],{"class":191},[160,747,748],{"class":195}," writer:\n",[160,750,751,754,757,759,762,765,767,770],{"class":74,"line":270},[160,752,753],{"class":195},"        df.to_excel(writer, ",[160,755,756],{"class":390},"sheet_name",[160,758,225],{"class":191},[160,760,761],{"class":195},"sheet_name, ",[160,763,764],{"class":390},"index",[160,766,225],{"class":191},[160,768,769],{"class":396},"False",[160,771,256],{"class":195},[160,773,774],{"class":74,"line":279},[160,775,209],{"emptyLinePlaceholder":208},[160,777,778,780,782],{"class":74,"line":285},[160,779,377],{"class":195},[160,781,225],{"class":191},[160,783,382],{"class":195},[160,785,786,788,790,792,794,796,798,800,802,804,806,808,810,812],{"class":74,"line":428},[160,787,448],{"class":195},[160,789,225],{"class":191},[160,791,453],{"class":195},[160,793,456],{"class":390},[160,795,225],{"class":191},[160,797,461],{"class":195},[160,799,464],{"class":390},[160,801,225],{"class":191},[160,803,469],{"class":169},[160,805,400],{"class":195},[160,807,474],{"class":390},[160,809,225],{"class":191},[160,811,479],{"class":169},[160,813,256],{"class":195},[160,815,816,818,820],{"class":74,"line":433},[160,817,487],{"class":195},[160,819,225],{"class":191},[160,821,492],{"class":195},[160,823,824,826],{"class":74,"line":439},[160,825,498],{"class":191},[160,827,501],{"class":195},[160,829,830,832,834,836,838,840],{"class":74,"line":445},[160,831,507],{"class":191},[160,833,510],{"class":195},[160,835,513],{"class":169},[160,837,516],{"class":195},[160,839,519],{"class":191},[160,841,842],{"class":195}," out:\n",[160,844,845],{"class":74,"line":484},[160,846,847],{"class":195},"            out.write(buffer.getvalue())\n",[160,849,850],{"class":74,"line":495},[160,851,577],{"class":195},[160,853,854],{"class":74,"line":504},[160,855,856],{"class":195},"            os.fsync(out.fileno())\n",[160,858,859,862,864],{"class":74,"line":541},[160,860,861],{"class":195},"        os.replace(tmp, share_dir ",[160,863,422],{"class":191},[160,865,866],{"class":195}," name)\n",[160,868,869,871,873],{"class":74,"line":568},[160,870,606],{"class":191},[160,872,609],{"class":396},[160,874,501],{"class":195},[160,876,877,879,881,883,885],{"class":74,"line":574},[160,878,617],{"class":195},[160,880,620],{"class":390},[160,882,225],{"class":191},[160,884,397],{"class":396},[160,886,256],{"class":195},[160,888,889],{"class":74,"line":580},[160,890,636],{"class":191},[160,892,893,895,897,899],{"class":74,"line":589},[160,894,642],{"class":191},[160,896,419],{"class":195},[160,898,422],{"class":191},[160,900,425],{"class":195},[145,902,904],{"id":903},"step-2-handle-a-file-somebody-has-open","Step 2 — Handle a file somebody has open",[10,906,907,908,911],{},"On Windows, Excel holds an exclusive lock while a workbook is open, and even the rename fails. Detect it and report clearly rather than letting the job die with a bare ",[157,909,910],{},"PermissionError",":",[20,913,30,919,30,922,30,925,30,928,30,932,30,938,30,942,30,954,30,962,30,969,30,974,30,978,30,983,30,987,30,991,30,995,30,999,30,1005,30,1010,30,1014],{"viewBox":914,"role":23,"ariaLabel":915,"ariaLabelledBy":916,"xmlns":28,"style":29},"0 0 800 226","Lock detection: attempt an exclusive open on the target, and on failure report who has it open using the owner file Excel leaves beside it.",[917,918],"lock2-t","lock2-d",[32,920,921],{"id":917},"Detecting a locked report before the publish fails",[36,923,924],{"id":918},"Before renaming, the job tries to open the target file for append, which requires the same exclusive access the rename needs. If that succeeds the file is free and the publish proceeds. If it raises a permission error the file is open in Excel; the job then reads the hidden owner file Excel leaves beside the workbook to name the person holding it, and raises a message an operator can act on rather than a bare traceback.",[40,926],{"x":42,"y":42,"width":43,"height":927,"fill":45},"226",[40,929],{"x":48,"y":930,"width":931,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"80","186",[57,933,937],{"x":934,"y":935,"style":936},"107","110","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","try an exclusive open",[57,939,941],{"x":934,"y":940,"style":71},"131","on the target file",[943,944,946,947,946,951,30],"g",{"stroke":54,"style":55,"fill":945},"none","\n    ",[948,949],"path",{"d":950},"M200 100 H 240 V 52 H 276",[948,952],{"d":953},"M200 130 H 240 V 174 H 276",[943,955,946,956,946,959,30],{"fill":83},[80,957],{"points":958},"284,52 272,46 272,58",[80,960],{"points":961},"284,174 272,168 272,180",[40,963],{"x":964,"y":965,"width":966,"height":967,"rx":968,"fill":122,"stroke":123,"style":55},"292","26","234","52","11",[57,970,973],{"x":971,"y":972,"style":130},"409","48","succeeds → file is free",[57,975,977],{"x":971,"y":976,"style":71},"67","rename into place",[40,979],{"x":964,"y":980,"width":966,"height":967,"rx":968,"fill":981,"stroke":982,"style":55},"148","#fee8f2","var(--accent,#f43f8f)",[57,984,910],{"x":971,"y":985,"style":986},"170","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[57,988,990],{"x":971,"y":989,"style":71},"189","someone has it open",[74,992],{"x1":993,"y1":76,"x2":994,"y2":76,"stroke":982,"style":55},"526","560",[80,996],{"points":997,"fill":998},"568,174 556,168 556,180","#f43f8f",[40,1000],{"x":1001,"y":1002,"width":1003,"height":60,"rx":1004,"fill":89,"stroke":90,"style":55},"576","140","208","12",[57,1006,1009],{"x":1007,"y":1008,"style":99},"680","166","read the owner file",[57,1011,1013],{"x":1007,"y":1012,"style":71},"188","name who is holding it",[57,1015,1016],{"x":140,"y":88,"style":71},"an actionable message beats a bare traceback at three in the morning",[150,1018,1020],{"className":182,"code":1019,"language":184,"meta":155,"style":155},"from pathlib import Path\n\ndef is_locked(target):\n    \"\"\"True if the file exists and cannot be opened exclusively.\"\"\"\n    target = Path(target)\n    if not target.exists():\n        return False\n    try:\n        with target.open(\"ab\"):\n            return False\n    except (PermissionError, OSError):\n        return True\n\ndef lock_holder(target):\n    \"\"\"Excel leaves a hidden owner file beside an open workbook.\"\"\"\n    target = Path(target)\n    owner = target.with_name(\"~$\" + target.name)\n    if not owner.exists():\n        return None\n    try:\n        # The user name sits near the start, UTF-16 encoded.\n        raw = owner.read_bytes()[:120]\n        return raw.decode(\"utf-16\", errors=\"ignore\").strip(\"\\x00 \").strip()\n    except OSError:\n        return None\n\ndef publish_checked(local_path, share_dir, name):\n    target = Path(share_dir) \u002F name\n    if is_locked(target):\n        who = lock_holder(target)\n        raise RuntimeError(\n            f\"{target} is open in Excel\"\n            + (f\" by {who}\" if who else \"\")\n            + \" — the report was not published.\"\n        )\n    return publish_atomic(local_path, share_dir, name)\n",[157,1021,1022,1032,1036,1046,1051,1060,1071,1079,1085,1097,1104,1120,1127,1131,1140,1145,1153,1172,1181,1188,1194,1199,1215,1250,1259,1265,1269,1279,1293,1301,1312,1324,1344,1381,1389,1395],{"__ignoreMap":155},[160,1023,1024,1026,1028,1030],{"class":74,"line":162},[160,1025,192],{"class":191},[160,1027,196],{"class":195},[160,1029,199],{"class":191},[160,1031,202],{"class":195},[160,1033,1034],{"class":74,"line":205},[160,1035,209],{"emptyLinePlaceholder":208},[160,1037,1038,1040,1043],{"class":74,"line":212},[160,1039,360],{"class":191},[160,1041,1042],{"class":363}," is_locked",[160,1044,1045],{"class":195},"(target):\n",[160,1047,1048],{"class":74,"line":219},[160,1049,1050],{"class":169},"    \"\"\"True if the file exists and cannot be opened exclusively.\"\"\"\n",[160,1052,1053,1055,1057],{"class":74,"line":259},[160,1054,414],{"class":195},[160,1056,225],{"class":191},[160,1058,1059],{"class":195}," Path(target)\n",[160,1061,1062,1065,1068],{"class":74,"line":264},[160,1063,1064],{"class":191},"    if",[160,1066,1067],{"class":191}," not",[160,1069,1070],{"class":195}," target.exists():\n",[160,1072,1073,1076],{"class":74,"line":270},[160,1074,1075],{"class":191},"        return",[160,1077,1078],{"class":396}," False\n",[160,1080,1081,1083],{"class":74,"line":279},[160,1082,498],{"class":191},[160,1084,501],{"class":195},[160,1086,1087,1089,1092,1095],{"class":74,"line":285},[160,1088,507],{"class":191},[160,1090,1091],{"class":195}," target.open(",[160,1093,1094],{"class":169},"\"ab\"",[160,1096,565],{"class":195},[160,1098,1099,1102],{"class":74,"line":428},[160,1100,1101],{"class":191},"            return",[160,1103,1078],{"class":396},[160,1105,1106,1108,1111,1113,1115,1118],{"class":74,"line":433},[160,1107,606],{"class":191},[160,1109,1110],{"class":195}," (",[160,1112,910],{"class":396},[160,1114,400],{"class":195},[160,1116,1117],{"class":396},"OSError",[160,1119,565],{"class":195},[160,1121,1122,1124],{"class":74,"line":439},[160,1123,1075],{"class":191},[160,1125,1126],{"class":396}," True\n",[160,1128,1129],{"class":74,"line":445},[160,1130,209],{"emptyLinePlaceholder":208},[160,1132,1133,1135,1138],{"class":74,"line":484},[160,1134,360],{"class":191},[160,1136,1137],{"class":363}," lock_holder",[160,1139,1045],{"class":195},[160,1141,1142],{"class":74,"line":495},[160,1143,1144],{"class":169},"    \"\"\"Excel leaves a hidden owner file beside an open workbook.\"\"\"\n",[160,1146,1147,1149,1151],{"class":74,"line":504},[160,1148,414],{"class":195},[160,1150,225],{"class":191},[160,1152,1059],{"class":195},[160,1154,1155,1158,1160,1163,1166,1169],{"class":74,"line":541},[160,1156,1157],{"class":195},"    owner ",[160,1159,225],{"class":191},[160,1161,1162],{"class":195}," target.with_name(",[160,1164,1165],{"class":169},"\"~$\"",[160,1167,1168],{"class":191}," +",[160,1170,1171],{"class":195}," target.name)\n",[160,1173,1174,1176,1178],{"class":74,"line":568},[160,1175,1064],{"class":191},[160,1177,1067],{"class":191},[160,1179,1180],{"class":195}," owner.exists():\n",[160,1182,1183,1185],{"class":74,"line":574},[160,1184,1075],{"class":191},[160,1186,1187],{"class":396}," None\n",[160,1189,1190,1192],{"class":74,"line":580},[160,1191,498],{"class":191},[160,1193,501],{"class":195},[160,1195,1196],{"class":74,"line":589},[160,1197,1198],{"class":215},"        # The user name sits near the start, UTF-16 encoded.\n",[160,1200,1201,1204,1206,1209,1212],{"class":74,"line":594},[160,1202,1203],{"class":195},"        raw ",[160,1205,225],{"class":191},[160,1207,1208],{"class":195}," owner.read_bytes()[:",[160,1210,1211],{"class":396},"120",[160,1213,1214],{"class":195},"]\n",[160,1216,1217,1219,1222,1225,1227,1230,1232,1235,1238,1240,1244,1247],{"class":74,"line":603},[160,1218,1075],{"class":191},[160,1220,1221],{"class":195}," raw.decode(",[160,1223,1224],{"class":169},"\"utf-16\"",[160,1226,400],{"class":195},[160,1228,1229],{"class":390},"errors",[160,1231,225],{"class":191},[160,1233,1234],{"class":169},"\"ignore\"",[160,1236,1237],{"class":195},").strip(",[160,1239,234],{"class":169},[160,1241,1243],{"class":1242},"sSjpA","\\x00",[160,1245,1246],{"class":169}," \"",[160,1248,1249],{"class":195},").strip()\n",[160,1251,1252,1254,1257],{"class":74,"line":614},[160,1253,606],{"class":191},[160,1255,1256],{"class":396}," OSError",[160,1258,501],{"class":195},[160,1260,1261,1263],{"class":74,"line":633},[160,1262,1075],{"class":191},[160,1264,1187],{"class":396},[160,1266,1267],{"class":74,"line":639},[160,1268,209],{"emptyLinePlaceholder":208},[160,1270,1272,1274,1277],{"class":74,"line":1271},27,[160,1273,360],{"class":191},[160,1275,1276],{"class":363}," publish_checked",[160,1278,367],{"class":195},[160,1280,1282,1284,1286,1289,1291],{"class":74,"line":1281},28,[160,1283,414],{"class":195},[160,1285,225],{"class":191},[160,1287,1288],{"class":195}," Path(share_dir) ",[160,1290,422],{"class":191},[160,1292,425],{"class":195},[160,1294,1296,1298],{"class":74,"line":1295},29,[160,1297,1064],{"class":191},[160,1299,1300],{"class":195}," is_locked(target):\n",[160,1302,1304,1307,1309],{"class":74,"line":1303},30,[160,1305,1306],{"class":195},"        who ",[160,1308,225],{"class":191},[160,1310,1311],{"class":195}," lock_holder(target)\n",[160,1313,1315,1318,1321],{"class":74,"line":1314},31,[160,1316,1317],{"class":191},"        raise",[160,1319,1320],{"class":396}," RuntimeError",[160,1322,1323],{"class":195},"(\n",[160,1325,1327,1330,1332,1335,1338,1341],{"class":74,"line":1326},32,[160,1328,1329],{"class":191},"            f",[160,1331,234],{"class":169},[160,1333,1334],{"class":1242},"{",[160,1336,1337],{"class":195},"target",[160,1339,1340],{"class":1242},"}",[160,1342,1343],{"class":169}," is open in Excel\"\n",[160,1345,1347,1350,1352,1355,1358,1360,1363,1365,1367,1370,1373,1376,1379],{"class":74,"line":1346},33,[160,1348,1349],{"class":191},"            +",[160,1351,1110],{"class":195},[160,1353,1354],{"class":191},"f",[160,1356,1357],{"class":169},"\" by ",[160,1359,1334],{"class":1242},[160,1361,1362],{"class":195},"who",[160,1364,1340],{"class":1242},[160,1366,234],{"class":169},[160,1368,1369],{"class":191}," if",[160,1371,1372],{"class":195}," who ",[160,1374,1375],{"class":191},"else",[160,1377,1378],{"class":169}," \"\"",[160,1380,256],{"class":195},[160,1382,1384,1386],{"class":74,"line":1383},34,[160,1385,1349],{"class":191},[160,1387,1388],{"class":169}," \" — the report was not published.\"\n",[160,1390,1392],{"class":74,"line":1391},35,[160,1393,1394],{"class":195},"        )\n",[160,1396,1398,1400],{"class":74,"line":1397},36,[160,1399,642],{"class":191},[160,1401,1402],{"class":195}," publish_atomic(local_path, share_dir, name)\n",[10,1404,1405,1406,1409],{},"LibreOffice uses a different convention, a ",[157,1407,1408],{},".~lock.report.xlsx#"," file, so ignore both patterns when listing a folder or you will treat lock files as reports.",[10,1411,1412],{},"A brief retry is worth adding, because the lock is often momentary — somebody glancing at last month's figures. Give it a few minutes, then give up loudly:",[150,1414,1416],{"className":182,"code":1415,"language":184,"meta":155,"style":155},"import time\n\ndef publish_waiting(local_path, share_dir, name, attempts=6, gap=30):\n    target = Path(share_dir) \u002F name\n    for attempt in range(1, attempts + 1):\n        if not is_locked(target):\n            return publish_atomic(local_path, share_dir, name)\n        if attempt \u003C attempts:\n            print(f\"{target.name} is locked; retrying in {gap}s \"\n                  f\"({attempt}\u002F{attempts})\")\n            time.sleep(gap)\n    raise RuntimeError(\n        f\"{target} stayed locked for {attempts * gap}s\"\n        + (f\"; held by {lock_holder(target)}\" if lock_holder(target) else \"\")\n    )\n",[157,1417,1418,1425,1429,1454,1466,1496,1505,1511,1523,1554,1583,1588,1597,1629,1661],{"__ignoreMap":155},[160,1419,1420,1422],{"class":74,"line":162},[160,1421,199],{"class":191},[160,1423,1424],{"class":195}," time\n",[160,1426,1427],{"class":74,"line":205},[160,1428,209],{"emptyLinePlaceholder":208},[160,1430,1431,1433,1436,1439,1441,1444,1447,1449,1452],{"class":74,"line":212},[160,1432,360],{"class":191},[160,1434,1435],{"class":363}," publish_waiting",[160,1437,1438],{"class":195},"(local_path, share_dir, name, attempts",[160,1440,225],{"class":191},[160,1442,1443],{"class":396},"6",[160,1445,1446],{"class":195},", gap",[160,1448,225],{"class":191},[160,1450,1451],{"class":396},"30",[160,1453,565],{"class":195},[160,1455,1456,1458,1460,1462,1464],{"class":74,"line":219},[160,1457,414],{"class":195},[160,1459,225],{"class":191},[160,1461,1288],{"class":195},[160,1463,422],{"class":191},[160,1465,425],{"class":195},[160,1467,1468,1471,1474,1477,1480,1483,1485,1488,1491,1494],{"class":74,"line":259},[160,1469,1470],{"class":191},"    for",[160,1472,1473],{"class":195}," attempt ",[160,1475,1476],{"class":191},"in",[160,1478,1479],{"class":396}," range",[160,1481,1482],{"class":195},"(",[160,1484,556],{"class":396},[160,1486,1487],{"class":195},", attempts ",[160,1489,1490],{"class":191},"+",[160,1492,1493],{"class":396}," 1",[160,1495,565],{"class":195},[160,1497,1498,1501,1503],{"class":74,"line":264},[160,1499,1500],{"class":191},"        if",[160,1502,1067],{"class":191},[160,1504,1300],{"class":195},[160,1506,1507,1509],{"class":74,"line":270},[160,1508,1101],{"class":191},[160,1510,1402],{"class":195},[160,1512,1513,1515,1517,1520],{"class":74,"line":279},[160,1514,1500],{"class":191},[160,1516,1473],{"class":195},[160,1518,1519],{"class":191},"\u003C",[160,1521,1522],{"class":195}," attempts:\n",[160,1524,1525,1528,1530,1532,1534,1536,1539,1541,1544,1546,1549,1551],{"class":74,"line":285},[160,1526,1527],{"class":396},"            print",[160,1529,1482],{"class":195},[160,1531,1354],{"class":191},[160,1533,234],{"class":169},[160,1535,1334],{"class":1242},[160,1537,1538],{"class":195},"target.name",[160,1540,1340],{"class":1242},[160,1542,1543],{"class":169}," is locked; retrying in ",[160,1545,1334],{"class":1242},[160,1547,1548],{"class":195},"gap",[160,1550,1340],{"class":1242},[160,1552,1553],{"class":169},"s \"\n",[160,1555,1556,1559,1562,1564,1567,1569,1571,1573,1576,1578,1581],{"class":74,"line":428},[160,1557,1558],{"class":191},"                  f",[160,1560,1561],{"class":169},"\"(",[160,1563,1334],{"class":1242},[160,1565,1566],{"class":195},"attempt",[160,1568,1340],{"class":1242},[160,1570,422],{"class":169},[160,1572,1334],{"class":1242},[160,1574,1575],{"class":195},"attempts",[160,1577,1340],{"class":1242},[160,1579,1580],{"class":169},")\"",[160,1582,256],{"class":195},[160,1584,1585],{"class":74,"line":433},[160,1586,1587],{"class":195},"            time.sleep(gap)\n",[160,1589,1590,1593,1595],{"class":74,"line":439},[160,1591,1592],{"class":191},"    raise",[160,1594,1320],{"class":396},[160,1596,1323],{"class":195},[160,1598,1599,1602,1604,1606,1608,1610,1613,1615,1618,1621,1624,1626],{"class":74,"line":445},[160,1600,1601],{"class":191},"        f",[160,1603,234],{"class":169},[160,1605,1334],{"class":1242},[160,1607,1337],{"class":195},[160,1609,1340],{"class":1242},[160,1611,1612],{"class":169}," stayed locked for ",[160,1614,1334],{"class":1242},[160,1616,1617],{"class":195},"attempts ",[160,1619,1620],{"class":191},"*",[160,1622,1623],{"class":195}," gap",[160,1625,1340],{"class":1242},[160,1627,1628],{"class":169},"s\"\n",[160,1630,1631,1634,1636,1638,1641,1643,1646,1648,1650,1652,1655,1657,1659],{"class":74,"line":484},[160,1632,1633],{"class":191},"        +",[160,1635,1110],{"class":195},[160,1637,1354],{"class":191},[160,1639,1640],{"class":169},"\"; held by ",[160,1642,1334],{"class":1242},[160,1644,1645],{"class":195},"lock_holder(target)",[160,1647,1340],{"class":1242},[160,1649,234],{"class":169},[160,1651,1369],{"class":191},[160,1653,1654],{"class":195}," lock_holder(target) ",[160,1656,1375],{"class":191},[160,1658,1378],{"class":169},[160,1660,256],{"class":195},[160,1662,1663],{"class":74,"line":495},[160,1664,1665],{"class":195},"    )\n",[10,1667,1668,1669,1672,1673,1676],{},"Publishing to a ",[300,1670,1671],{},"dated"," name sidesteps the problem entirely, since nobody can have a file open that does not exist yet. Reserve the stable ",[157,1674,1675],{},"-latest"," name for the copy that might be locked, and let the dated write always succeed.",[145,1678,1680],{"id":1679},"step-3-fail-early-on-an-unreachable-share","Step 3 — Fail early on an unreachable share",[10,1682,1683],{},"A share that is not mounted looks like an empty directory on Linux, so a naive script writes the report into the local mount point and reports success. Check before building:",[150,1685,1687],{"className":182,"code":1686,"language":184,"meta":155,"style":155},"from pathlib import Path\n\ndef check_share(share_dir, marker=\".reports-share\"):\n    \"\"\"Verify the share is mounted and writable before doing any work.\"\"\"\n    share = Path(share_dir)\n\n    if not share.is_dir():\n        raise RuntimeError(f\"{share} is not reachable — is the share mounted?\")\n\n    # A marker file placed on the share itself proves it is the real target\n    # rather than an empty local mount point.\n    if not (share \u002F marker).exists():\n        raise RuntimeError(\n            f\"{share} is missing {marker} — this looks like an unmounted \"\n            \"mount point, not the share.\"\n        )\n\n    probe = share \u002F f\".write-probe-{os.getpid()}\"\n    try:\n        probe.write_bytes(b\"ok\")\n    except OSError as exc:\n        raise RuntimeError(f\"{share} is not writable: {exc}\") from None\n    finally:\n        probe.unlink(missing_ok=True)\n\n    return True\n",[157,1688,1689,1699,1703,1720,1725,1734,1738,1747,1771,1775,1780,1785,1799,1807,1832,1837,1841,1845,1873,1879,1892,1904,1940,1947,1960,1964],{"__ignoreMap":155},[160,1690,1691,1693,1695,1697],{"class":74,"line":162},[160,1692,192],{"class":191},[160,1694,196],{"class":195},[160,1696,199],{"class":191},[160,1698,202],{"class":195},[160,1700,1701],{"class":74,"line":205},[160,1702,209],{"emptyLinePlaceholder":208},[160,1704,1705,1707,1710,1713,1715,1718],{"class":74,"line":212},[160,1706,360],{"class":191},[160,1708,1709],{"class":363}," check_share",[160,1711,1712],{"class":195},"(share_dir, marker",[160,1714,225],{"class":191},[160,1716,1717],{"class":169},"\".reports-share\"",[160,1719,565],{"class":195},[160,1721,1722],{"class":74,"line":219},[160,1723,1724],{"class":169},"    \"\"\"Verify the share is mounted and writable before doing any work.\"\"\"\n",[160,1726,1727,1730,1732],{"class":74,"line":259},[160,1728,1729],{"class":195},"    share ",[160,1731,225],{"class":191},[160,1733,382],{"class":195},[160,1735,1736],{"class":74,"line":264},[160,1737,209],{"emptyLinePlaceholder":208},[160,1739,1740,1742,1744],{"class":74,"line":270},[160,1741,1064],{"class":191},[160,1743,1067],{"class":191},[160,1745,1746],{"class":195}," share.is_dir():\n",[160,1748,1749,1751,1753,1755,1757,1759,1761,1764,1766,1769],{"class":74,"line":279},[160,1750,1317],{"class":191},[160,1752,1320],{"class":396},[160,1754,1482],{"class":195},[160,1756,1354],{"class":191},[160,1758,234],{"class":169},[160,1760,1334],{"class":1242},[160,1762,1763],{"class":195},"share",[160,1765,1340],{"class":1242},[160,1767,1768],{"class":169}," is not reachable — is the share mounted?\"",[160,1770,256],{"class":195},[160,1772,1773],{"class":74,"line":285},[160,1774,209],{"emptyLinePlaceholder":208},[160,1776,1777],{"class":74,"line":428},[160,1778,1779],{"class":215},"    # A marker file placed on the share itself proves it is the real target\n",[160,1781,1782],{"class":74,"line":433},[160,1783,1784],{"class":215},"    # rather than an empty local mount point.\n",[160,1786,1787,1789,1791,1794,1796],{"class":74,"line":439},[160,1788,1064],{"class":191},[160,1790,1067],{"class":191},[160,1792,1793],{"class":195}," (share ",[160,1795,422],{"class":191},[160,1797,1798],{"class":195}," marker).exists():\n",[160,1800,1801,1803,1805],{"class":74,"line":445},[160,1802,1317],{"class":191},[160,1804,1320],{"class":396},[160,1806,1323],{"class":195},[160,1808,1809,1811,1813,1815,1817,1819,1822,1824,1827,1829],{"class":74,"line":484},[160,1810,1329],{"class":191},[160,1812,234],{"class":169},[160,1814,1334],{"class":1242},[160,1816,1763],{"class":195},[160,1818,1340],{"class":1242},[160,1820,1821],{"class":169}," is missing ",[160,1823,1334],{"class":1242},[160,1825,1826],{"class":195},"marker",[160,1828,1340],{"class":1242},[160,1830,1831],{"class":169}," — this looks like an unmounted \"\n",[160,1833,1834],{"class":74,"line":495},[160,1835,1836],{"class":169},"            \"mount point, not the share.\"\n",[160,1838,1839],{"class":74,"line":504},[160,1840,1394],{"class":195},[160,1842,1843],{"class":74,"line":541},[160,1844,209],{"emptyLinePlaceholder":208},[160,1846,1847,1850,1852,1855,1857,1860,1863,1865,1868,1870],{"class":74,"line":568},[160,1848,1849],{"class":195},"    probe ",[160,1851,225],{"class":191},[160,1853,1854],{"class":195}," share ",[160,1856,422],{"class":191},[160,1858,1859],{"class":191}," f",[160,1861,1862],{"class":169},"\".write-probe-",[160,1864,1334],{"class":1242},[160,1866,1867],{"class":195},"os.getpid()",[160,1869,1340],{"class":1242},[160,1871,1872],{"class":169},"\"\n",[160,1874,1875,1877],{"class":74,"line":574},[160,1876,498],{"class":191},[160,1878,501],{"class":195},[160,1880,1881,1884,1887,1890],{"class":74,"line":580},[160,1882,1883],{"class":195},"        probe.write_bytes(",[160,1885,1886],{"class":191},"b",[160,1888,1889],{"class":169},"\"ok\"",[160,1891,256],{"class":195},[160,1893,1894,1896,1898,1901],{"class":74,"line":589},[160,1895,606],{"class":191},[160,1897,1256],{"class":396},[160,1899,1900],{"class":191}," as",[160,1902,1903],{"class":195}," exc:\n",[160,1905,1906,1908,1910,1912,1914,1916,1918,1920,1922,1925,1927,1930,1932,1934,1936,1938],{"class":74,"line":594},[160,1907,1317],{"class":191},[160,1909,1320],{"class":396},[160,1911,1482],{"class":195},[160,1913,1354],{"class":191},[160,1915,234],{"class":169},[160,1917,1334],{"class":1242},[160,1919,1763],{"class":195},[160,1921,1340],{"class":1242},[160,1923,1924],{"class":169}," is not writable: ",[160,1926,1334],{"class":1242},[160,1928,1929],{"class":195},"exc",[160,1931,1340],{"class":1242},[160,1933,234],{"class":169},[160,1935,516],{"class":195},[160,1937,192],{"class":191},[160,1939,1187],{"class":396},[160,1941,1942,1945],{"class":74,"line":603},[160,1943,1944],{"class":191},"    finally",[160,1946,501],{"class":195},[160,1948,1949,1952,1954,1956,1958],{"class":74,"line":614},[160,1950,1951],{"class":195},"        probe.unlink(",[160,1953,620],{"class":390},[160,1955,225],{"class":191},[160,1957,397],{"class":396},[160,1959,256],{"class":195},[160,1961,1962],{"class":74,"line":633},[160,1963,209],{"emptyLinePlaceholder":208},[160,1965,1966,1968],{"class":74,"line":639},[160,1967,642],{"class":191},[160,1969,1126],{"class":396},[10,1971,1972,1973,1976],{},"The marker file is the part worth copying. Checking ",[157,1974,1975],{},"is_dir()"," alone passes on an empty local directory where the share should be, which is precisely the silent-success case.",[145,1978,1980],{"id":1979},"step-4-retain-history-without-filling-the-share","Step 4 — Retain history without filling the share",[20,1982,30,1988,30,1991,30,1994,30,1997,30,2002,30,2007,30,2012,30,2015,30,2018,30,2023,30,2026,30,2031,30,2036,30,2039,30,2042,30,2045,30,2048,30,2051,30,2054,30,2057,30,2060,30,2063,30,2066,30,2069,30,2071,30,2074,30,2077,30,2080,30,2083,30,2088,30,2094,30,2097,30,2100,30,2103,30,2106],{"viewBox":1983,"role":23,"ariaLabel":1984,"ariaLabelledBy":1985,"xmlns":28,"style":29},"0 0 800 238","Retention policy over a year of dated reports: recent files are kept in full, older months keep one file each, and everything else is pruned.",[1986,1987],"ret-t","ret-d",[32,1989,1990],{"id":1986},"A retention policy that keeps history without filling the share",[36,1992,1993],{"id":1987},"A timeline of dated reports across a year. Files from the last ninety days are kept in full, so recent detail is always available. For older periods only the first report of each month is kept as an archive marker. Everything else is pruned. The stable latest file is never touched by the policy, and the prune function defaults to a dry run so the deletion list can be reviewed first.",[40,1995],{"x":42,"y":42,"width":43,"height":1996,"fill":45},"238",[57,1998,2001],{"x":140,"y":1999,"style":2000},"28","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","older ←———————————————————————————————————————————————→ newer",[40,2003],{"x":2004,"y":2005,"width":2006,"height":87,"rx":1004,"fill":981,"stroke":982,"style":55},"20","44","452",[57,2008,2011],{"x":2009,"y":51,"style":2010},"246","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","older than 90 days",[57,2013,2014],{"x":2009,"y":59,"style":71},"pruned, except one keeper per month",[40,2016],{"x":2017,"y":2005,"width":964,"height":87,"rx":1004,"fill":122,"stroke":123,"style":55},"488",[57,2019,2022],{"x":2020,"y":51,"style":2021},"634","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","last 90 days",[57,2024,2025],{"x":2020,"y":59,"style":71},"kept in full",[40,2027],{"x":2028,"y":2029,"width":965,"height":965,"rx":2030,"fill":89,"stroke":90,"style":55},"40","132","5",[40,2032],{"x":2033,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"76","#f0f2f5","var(--line,#cdd5e6)",[40,2037],{"x":2038,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"112",[40,2040],{"x":2041,"y":2029,"width":965,"height":965,"rx":2030,"fill":89,"stroke":90,"style":55},"156",[40,2043],{"x":2044,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"192",[40,2046],{"x":2047,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"228",[40,2049],{"x":2050,"y":2029,"width":965,"height":965,"rx":2030,"fill":89,"stroke":90,"style":55},"272",[40,2052],{"x":2053,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"308",[40,2055],{"x":2056,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"344",[40,2058],{"x":2059,"y":2029,"width":965,"height":965,"rx":2030,"fill":89,"stroke":90,"style":55},"388",[40,2061],{"x":2062,"y":2029,"width":965,"height":965,"rx":2030,"fill":2034,"stroke":2035},"424",[40,2064],{"x":2065,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"504",[40,2067],{"x":2068,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"540",[40,2070],{"x":1001,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},[40,2072],{"x":2073,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"612",[40,2075],{"x":2076,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"648",[40,2078],{"x":2079,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"684",[40,2081],{"x":2082,"y":2029,"width":965,"height":965,"rx":2030,"fill":122,"stroke":123,"style":55},"720",[40,2084],{"x":2028,"y":2085,"width":2086,"height":2086,"rx":2087,"fill":89,"stroke":90,"style":55},"180","18","4",[57,2089,2093],{"x":2090,"y":2091,"style":2092},"66","194","font-size:10.5px;fill:var(--muted,#5b6780)","monthly keeper",[40,2095],{"x":2096,"y":2085,"width":2086,"height":2086,"rx":2087,"fill":2034,"stroke":2035},"200",[57,2098,2099],{"x":927,"y":2091,"style":2092},"pruned",[40,2101],{"x":2102,"y":2085,"width":2086,"height":2086,"rx":2087,"fill":122,"stroke":123,"style":55},"340",[57,2104,2025],{"x":2105,"y":2091,"style":2092},"366",[57,2107,2110],{"x":140,"y":2108,"style":2109},"224","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","the stable \"latest\" file is never touched by the policy",[10,2112,2113],{},"Dated reports accumulate. Prune on a schedule, keeping recent files and one per month for the archive:",[150,2115,2117],{"className":182,"code":2116,"language":184,"meta":155,"style":155},"import re\nfrom datetime import date, timedelta\nfrom pathlib import Path\n\nDATED = re.compile(r\"-(\\d{4})-(\\d{2})(?:-(\\d{2}))?\\.xlsx$\")\n\ndef prune(share_dir, keep_days=90, dry_run=True):\n    \"\"\"Delete dated reports older than keep_days, keeping the first of each month.\"\"\"\n    cutoff = date.today() - timedelta(days=keep_days)\n    removed = []\n\n    for path in sorted(Path(share_dir).glob(\"*.xlsx\")):\n        if path.name.startswith((\"~$\", \".~lock\")):\n            continue                      # lock files, not reports\n        match = DATED.search(path.name)\n        if not match:\n            continue                      # not a dated report; leave it alone\n\n        year, month, day = match.groups()\n        stamp = date(int(year), int(month), int(day or 1))\n        if stamp >= cutoff or day in (None, \"01\"):\n            continue                      # recent, or the monthly keeper\n\n        removed.append(path)\n        if not dry_run:\n            path.unlink()\n\n    return removed\n\nfor path in prune(\"\u002Fmnt\u002Freports\u002Fmonthly\", dry_run=True):\n    print(\"would remove\", path.name)\n",[157,2118,2119,2126,2138,2148,2152,2213,2217,2241,2246,2269,2279,2283,2304,2320,2328,2341,2350,2357,2361,2371,2405,2437,2444,2448,2453,2462,2467,2471,2478,2482,2507],{"__ignoreMap":155},[160,2120,2121,2123],{"class":74,"line":162},[160,2122,199],{"class":191},[160,2124,2125],{"class":195}," re\n",[160,2127,2128,2130,2133,2135],{"class":74,"line":205},[160,2129,192],{"class":191},[160,2131,2132],{"class":195}," datetime ",[160,2134,199],{"class":191},[160,2136,2137],{"class":195}," date, timedelta\n",[160,2139,2140,2142,2144,2146],{"class":74,"line":212},[160,2141,192],{"class":191},[160,2143,196],{"class":195},[160,2145,199],{"class":191},[160,2147,202],{"class":195},[160,2149,2150],{"class":74,"line":219},[160,2151,209],{"emptyLinePlaceholder":208},[160,2153,2154,2157,2160,2163,2165,2168,2171,2174,2177,2180,2182,2185,2188,2190,2192,2194,2197,2200,2203,2206,2209,2211],{"class":74,"line":259},[160,2155,2156],{"class":396},"DATED",[160,2158,2159],{"class":191}," =",[160,2161,2162],{"class":195}," re.compile(",[160,2164,231],{"class":191},[160,2166,2167],{"class":169},"\"-",[160,2169,2170],{"class":396},"(\\d",[160,2172,2173],{"class":191},"{4}",[160,2175,2176],{"class":396},")",[160,2178,2179],{"class":169},"-",[160,2181,2170],{"class":396},[160,2183,2184],{"class":191},"{2}",[160,2186,2187],{"class":396},")(?:",[160,2189,2179],{"class":169},[160,2191,2170],{"class":396},[160,2193,2184],{"class":191},[160,2195,2196],{"class":396},"))",[160,2198,2199],{"class":191},"?",[160,2201,2202],{"class":237},"\\.",[160,2204,2205],{"class":169},"xlsx",[160,2207,2208],{"class":396},"$",[160,2210,234],{"class":169},[160,2212,256],{"class":195},[160,2214,2215],{"class":74,"line":264},[160,2216,209],{"emptyLinePlaceholder":208},[160,2218,2219,2221,2224,2227,2229,2232,2235,2237,2239],{"class":74,"line":270},[160,2220,360],{"class":191},[160,2222,2223],{"class":363}," prune",[160,2225,2226],{"class":195},"(share_dir, keep_days",[160,2228,225],{"class":191},[160,2230,2231],{"class":396},"90",[160,2233,2234],{"class":195},", dry_run",[160,2236,225],{"class":191},[160,2238,397],{"class":396},[160,2240,565],{"class":195},[160,2242,2243],{"class":74,"line":279},[160,2244,2245],{"class":169},"    \"\"\"Delete dated reports older than keep_days, keeping the first of each month.\"\"\"\n",[160,2247,2248,2251,2253,2256,2258,2261,2264,2266],{"class":74,"line":285},[160,2249,2250],{"class":195},"    cutoff ",[160,2252,225],{"class":191},[160,2254,2255],{"class":195}," date.today() ",[160,2257,2179],{"class":191},[160,2259,2260],{"class":195}," timedelta(",[160,2262,2263],{"class":390},"days",[160,2265,225],{"class":191},[160,2267,2268],{"class":195},"keep_days)\n",[160,2270,2271,2274,2276],{"class":74,"line":428},[160,2272,2273],{"class":195},"    removed ",[160,2275,225],{"class":191},[160,2277,2278],{"class":195}," []\n",[160,2280,2281],{"class":74,"line":433},[160,2282,209],{"emptyLinePlaceholder":208},[160,2284,2285,2287,2290,2292,2295,2298,2301],{"class":74,"line":439},[160,2286,1470],{"class":191},[160,2288,2289],{"class":195}," path ",[160,2291,1476],{"class":191},[160,2293,2294],{"class":396}," sorted",[160,2296,2297],{"class":195},"(Path(share_dir).glob(",[160,2299,2300],{"class":169},"\"*.xlsx\"",[160,2302,2303],{"class":195},")):\n",[160,2305,2306,2308,2311,2313,2315,2318],{"class":74,"line":445},[160,2307,1500],{"class":191},[160,2309,2310],{"class":195}," path.name.startswith((",[160,2312,1165],{"class":169},[160,2314,400],{"class":195},[160,2316,2317],{"class":169},"\".~lock\"",[160,2319,2303],{"class":195},[160,2321,2322,2325],{"class":74,"line":484},[160,2323,2324],{"class":191},"            continue",[160,2326,2327],{"class":215},"                      # lock files, not reports\n",[160,2329,2330,2333,2335,2338],{"class":74,"line":495},[160,2331,2332],{"class":195},"        match ",[160,2334,225],{"class":191},[160,2336,2337],{"class":396}," DATED",[160,2339,2340],{"class":195},".search(path.name)\n",[160,2342,2343,2345,2347],{"class":74,"line":504},[160,2344,1500],{"class":191},[160,2346,1067],{"class":191},[160,2348,2349],{"class":195}," match:\n",[160,2351,2352,2354],{"class":74,"line":541},[160,2353,2324],{"class":191},[160,2355,2356],{"class":215},"                      # not a dated report; leave it alone\n",[160,2358,2359],{"class":74,"line":568},[160,2360,209],{"emptyLinePlaceholder":208},[160,2362,2363,2366,2368],{"class":74,"line":574},[160,2364,2365],{"class":195},"        year, month, day ",[160,2367,225],{"class":191},[160,2369,2370],{"class":195}," match.groups()\n",[160,2372,2373,2376,2378,2381,2384,2387,2389,2392,2394,2397,2400,2402],{"class":74,"line":580},[160,2374,2375],{"class":195},"        stamp ",[160,2377,225],{"class":191},[160,2379,2380],{"class":195}," date(",[160,2382,2383],{"class":396},"int",[160,2385,2386],{"class":195},"(year), ",[160,2388,2383],{"class":396},[160,2390,2391],{"class":195},"(month), ",[160,2393,2383],{"class":396},[160,2395,2396],{"class":195},"(day ",[160,2398,2399],{"class":191},"or",[160,2401,1493],{"class":396},[160,2403,2404],{"class":195},"))\n",[160,2406,2407,2409,2412,2415,2418,2420,2423,2425,2427,2430,2432,2435],{"class":74,"line":589},[160,2408,1500],{"class":191},[160,2410,2411],{"class":195}," stamp ",[160,2413,2414],{"class":191},">=",[160,2416,2417],{"class":195}," cutoff ",[160,2419,2399],{"class":191},[160,2421,2422],{"class":195}," day ",[160,2424,1476],{"class":191},[160,2426,1110],{"class":195},[160,2428,2429],{"class":396},"None",[160,2431,400],{"class":195},[160,2433,2434],{"class":169},"\"01\"",[160,2436,565],{"class":195},[160,2438,2439,2441],{"class":74,"line":594},[160,2440,2324],{"class":191},[160,2442,2443],{"class":215},"                      # recent, or the monthly keeper\n",[160,2445,2446],{"class":74,"line":603},[160,2447,209],{"emptyLinePlaceholder":208},[160,2449,2450],{"class":74,"line":614},[160,2451,2452],{"class":195},"        removed.append(path)\n",[160,2454,2455,2457,2459],{"class":74,"line":633},[160,2456,1500],{"class":191},[160,2458,1067],{"class":191},[160,2460,2461],{"class":195}," dry_run:\n",[160,2463,2464],{"class":74,"line":639},[160,2465,2466],{"class":195},"            path.unlink()\n",[160,2468,2469],{"class":74,"line":1271},[160,2470,209],{"emptyLinePlaceholder":208},[160,2472,2473,2475],{"class":74,"line":1281},[160,2474,642],{"class":191},[160,2476,2477],{"class":195}," removed\n",[160,2479,2480],{"class":74,"line":1295},[160,2481,209],{"emptyLinePlaceholder":208},[160,2483,2484,2487,2489,2491,2494,2496,2498,2501,2503,2505],{"class":74,"line":1303},[160,2485,2486],{"class":191},"for",[160,2488,2289],{"class":195},[160,2490,1476],{"class":191},[160,2492,2493],{"class":195}," prune(",[160,2495,294],{"class":169},[160,2497,400],{"class":195},[160,2499,2500],{"class":390},"dry_run",[160,2502,225],{"class":191},[160,2504,397],{"class":396},[160,2506,565],{"class":195},[160,2508,2509,2512,2514,2517],{"class":74,"line":1314},[160,2510,2511],{"class":396},"    print",[160,2513,1482],{"class":195},[160,2515,2516],{"class":169},"\"would remove\"",[160,2518,2519],{"class":195},", path.name)\n",[10,2521,2522,2523,2526],{},"Default the function to ",[157,2524,2525],{},"dry_run=True",". A deletion pass over a share is exactly the code you want to read the output of before trusting, and the day somebody widens the glob pattern is the day that default saves the archive.",[145,2528,2530],{"id":2529},"common-pitfalls-and-fixes","Common pitfalls and fixes",[2532,2533,2534,2550],"table",{},[2535,2536,2537],"thead",{},[2538,2539,2540,2544,2547],"tr",{},[2541,2542,2543],"th",{},"Symptom",[2541,2545,2546],{},"Cause",[2541,2548,2549],{},"Fix",[2551,2552,2553,2567,2578,2591,2602,2613,2635,2655],"tbody",{},[2538,2554,2555,2561,2564],{},[2556,2557,2558,2560],"td",{},[157,2559,910],{}," on an existing file",[2556,2562,2563],{},"Open in Excel",[2556,2565,2566],{},"Detect the lock, retry briefly, report who holds it.",[2538,2568,2569,2572,2575],{},[2556,2570,2571],{},"Works interactively, fails under the scheduler",[2556,2573,2574],{},"Mapped drive letters are per-session",[2556,2576,2577],{},"Use the full UNC path.",[2538,2579,2580,2583,2586],{},[2556,2581,2582],{},"Readers see a truncated workbook",[2556,2584,2585],{},"Written directly to the target",[2556,2587,2588,2589,18],{},"Write to a temporary name and ",[157,2590,321],{},[2538,2592,2593,2596,2599],{},[2556,2594,2595],{},"Rename is slow and not atomic",[2556,2597,2598],{},"Temporary file on another filesystem",[2556,2600,2601],{},"Create it in the destination folder.",[2538,2603,2604,2607,2610],{},[2556,2605,2606],{},"Report written but nobody can find it",[2556,2608,2609],{},"Share not mounted; wrote to the mount point",[2556,2611,2612],{},"Check for a marker file before writing.",[2538,2614,2615,2621,2624],{},[2556,2616,2617,2620],{},[157,2618,2619],{},"~$report.xlsx"," treated as a report",[2556,2622,2623],{},"Excel lock file matched the glob",[2556,2625,2626,2627,2630,2631,2634],{},"Skip ",[157,2628,2629],{},"~$"," and ",[157,2632,2633],{},".~lock"," prefixes.",[2538,2636,2637,2644,2647],{},[2556,2638,2639,2640,2643],{},"Folder full of ",[157,2641,2642],{},".tmp"," files",[2556,2645,2646],{},"Failures left staging files behind",[2556,2648,2649,2650,422,2652,18],{},"Clean up in an ",[157,2651,662],{},[157,2653,2654],{},"finally",[2538,2656,2657,2662,2665],{},[2556,2658,2659],{},[157,2660,2661],{},"OSError: [Errno 28]",[2556,2663,2664],{},"Share is full",[2556,2666,2667],{},"Prune old reports; alert on free space.",[145,2669,2671],{"id":2670},"performance-and-scale-notes","Performance and scale notes",[10,2673,2674,2675,2678],{},"Network filesystems make metadata operations expensive. A local ",[157,2676,2677],{},"stat"," is microseconds; over SMB it is a round trip, so code that looks harmless locally becomes slow on a share.",[10,2680,2681,2682,2685,2686,2689],{},"The main rule is ",[300,2683,2684],{},"minimise round trips",". Listing a directory once and working from the result beats calling ",[157,2687,2688],{},"exists()"," per file:",[150,2691,2693],{"className":182,"code":2692,"language":184,"meta":155,"style":155},"from pathlib import Path\n\n# Slow on a share: one round trip per name.\npresent = [n for n in expected_names if (share \u002F n).exists()]\n\n# Fast: one listing, then in-memory lookups.\non_share = {p.name for p in share.iterdir()}\npresent = [n for n in expected_names if n in on_share]\n",[157,2694,2695,2705,2709,2714,2744,2748,2753,2773],{"__ignoreMap":155},[160,2696,2697,2699,2701,2703],{"class":74,"line":162},[160,2698,192],{"class":191},[160,2700,196],{"class":195},[160,2702,199],{"class":191},[160,2704,202],{"class":195},[160,2706,2707],{"class":74,"line":205},[160,2708,209],{"emptyLinePlaceholder":208},[160,2710,2711],{"class":74,"line":212},[160,2712,2713],{"class":215},"# Slow on a share: one round trip per name.\n",[160,2715,2716,2719,2721,2724,2726,2729,2731,2734,2737,2739,2741],{"class":74,"line":219},[160,2717,2718],{"class":195},"present ",[160,2720,225],{"class":191},[160,2722,2723],{"class":195}," [n ",[160,2725,2486],{"class":191},[160,2727,2728],{"class":195}," n ",[160,2730,1476],{"class":191},[160,2732,2733],{"class":195}," expected_names ",[160,2735,2736],{"class":191},"if",[160,2738,1793],{"class":195},[160,2740,422],{"class":191},[160,2742,2743],{"class":195}," n).exists()]\n",[160,2745,2746],{"class":74,"line":259},[160,2747,209],{"emptyLinePlaceholder":208},[160,2749,2750],{"class":74,"line":264},[160,2751,2752],{"class":215},"# Fast: one listing, then in-memory lookups.\n",[160,2754,2755,2758,2760,2763,2765,2768,2770],{"class":74,"line":270},[160,2756,2757],{"class":195},"on_share ",[160,2759,225],{"class":191},[160,2761,2762],{"class":195}," {p.name ",[160,2764,2486],{"class":191},[160,2766,2767],{"class":195}," p ",[160,2769,1476],{"class":191},[160,2771,2772],{"class":195}," share.iterdir()}\n",[160,2774,2775,2777,2779,2781,2783,2785,2787,2789,2791,2793,2795],{"class":74,"line":279},[160,2776,2718],{"class":195},[160,2778,225],{"class":191},[160,2780,2723],{"class":195},[160,2782,2486],{"class":191},[160,2784,2728],{"class":195},[160,2786,1476],{"class":191},[160,2788,2733],{"class":195},[160,2790,2736],{"class":191},[160,2792,2728],{"class":195},[160,2794,1476],{"class":191},[160,2796,2797],{"class":195}," on_share]\n",[10,2799,2800,2803,2804,2807],{},[300,2801,2802],{},"Build locally, copy once."," Writing a workbook directly to a share means every buffered flush crosses the network. Generate to a local temporary file, then transfer the finished bytes in one streamed copy — which is what ",[157,2805,2806],{},"publish_atomic"," above does:",[150,2809,2811],{"className":182,"code":2810,"language":184,"meta":155,"style":155},"import tempfile\nfrom pathlib import Path\n\nwith tempfile.TemporaryDirectory() as work:\n    local = Path(work) \u002F \"report.xlsx\"\n    df.to_excel(local, index=False, engine=\"xlsxwriter\")   # local disk speed\n    publish_atomic(local, \"\u002Fmnt\u002Freports\u002Fmonthly\", \"regional-2026-08.xlsx\")\n",[157,2812,2813,2819,2829,2833,2846,2861,2886],{"__ignoreMap":155},[160,2814,2815,2817],{"class":74,"line":162},[160,2816,199],{"class":191},[160,2818,341],{"class":195},[160,2820,2821,2823,2825,2827],{"class":74,"line":205},[160,2822,192],{"class":191},[160,2824,196],{"class":195},[160,2826,199],{"class":191},[160,2828,202],{"class":195},[160,2830,2831],{"class":74,"line":212},[160,2832,209],{"emptyLinePlaceholder":208},[160,2834,2835,2838,2841,2843],{"class":74,"line":219},[160,2836,2837],{"class":191},"with",[160,2839,2840],{"class":195}," tempfile.TemporaryDirectory() ",[160,2842,519],{"class":191},[160,2844,2845],{"class":195}," work:\n",[160,2847,2848,2851,2853,2856,2858],{"class":74,"line":259},[160,2849,2850],{"class":195},"    local ",[160,2852,225],{"class":191},[160,2854,2855],{"class":195}," Path(work) ",[160,2857,422],{"class":191},[160,2859,2860],{"class":169}," \"report.xlsx\"\n",[160,2862,2863,2866,2868,2870,2872,2874,2876,2878,2880,2883],{"class":74,"line":264},[160,2864,2865],{"class":195},"    df.to_excel(local, ",[160,2867,764],{"class":390},[160,2869,225],{"class":191},[160,2871,769],{"class":396},[160,2873,400],{"class":195},[160,2875,736],{"class":390},[160,2877,225],{"class":191},[160,2879,741],{"class":169},[160,2881,2882],{"class":195},")   ",[160,2884,2885],{"class":215},"# local disk speed\n",[160,2887,2888,2891,2893,2895,2898],{"class":74,"line":270},[160,2889,2890],{"class":195},"    publish_atomic(local, ",[160,2892,294],{"class":169},[160,2894,400],{"class":195},[160,2896,2897],{"class":169},"\"regional-2026-08.xlsx\"",[160,2899,256],{"class":195},[10,2901,2902,2905],{},[300,2903,2904],{},"Parallelise across files, modestly."," A fan-out of one report per region benefits from a small thread pool, since the copies are I\u002FO-bound — but a share is a shared resource, and eight concurrent writers usually make everyone slower rather than the job faster. Four is a reasonable ceiling.",[10,2907,2908,2909,2911,2912,911],{},"Finally, keep an eye on free space as a first-class concern. A full share fails every publish at once, and the failure arrives as a confusing ",[157,2910,1117],{}," rather than a clear message. Checking before writing turns it into an actionable alert, which is the same fail-fast reasoning applied throughout ",[14,2913,2915],{"href":2914},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","error handling and logging in Excel automation",[150,2917,2919],{"className":182,"code":2918,"language":184,"meta":155,"style":155},"import shutil\n\nusage = shutil.disk_usage(\"\u002Fmnt\u002Freports\")\nfree_pct = usage.free \u002F usage.total * 100\nif free_pct \u003C 5:\n    raise RuntimeError(f\"share is {free_pct:.1f}% free — prune before publishing\")\n",[157,2920,2921,2928,2932,2947,2967,2981],{"__ignoreMap":155},[160,2922,2923,2925],{"class":74,"line":162},[160,2924,199],{"class":191},[160,2926,2927],{"class":195}," shutil\n",[160,2929,2930],{"class":74,"line":205},[160,2931,209],{"emptyLinePlaceholder":208},[160,2933,2934,2937,2939,2942,2945],{"class":74,"line":212},[160,2935,2936],{"class":195},"usage ",[160,2938,225],{"class":191},[160,2940,2941],{"class":195}," shutil.disk_usage(",[160,2943,2944],{"class":169},"\"\u002Fmnt\u002Freports\"",[160,2946,256],{"class":195},[160,2948,2949,2952,2954,2957,2959,2962,2964],{"class":74,"line":219},[160,2950,2951],{"class":195},"free_pct ",[160,2953,225],{"class":191},[160,2955,2956],{"class":195}," usage.free ",[160,2958,422],{"class":191},[160,2960,2961],{"class":195}," usage.total ",[160,2963,1620],{"class":191},[160,2965,2966],{"class":396}," 100\n",[160,2968,2969,2971,2974,2976,2979],{"class":74,"line":259},[160,2970,2736],{"class":191},[160,2972,2973],{"class":195}," free_pct ",[160,2975,1519],{"class":191},[160,2977,2978],{"class":396}," 5",[160,2980,501],{"class":195},[160,2982,2983,2985,2987,2989,2991,2994,2996,2999,3002,3004,3007],{"class":74,"line":264},[160,2984,1592],{"class":191},[160,2986,1320],{"class":396},[160,2988,1482],{"class":195},[160,2990,1354],{"class":191},[160,2992,2993],{"class":169},"\"share is ",[160,2995,1334],{"class":1242},[160,2997,2998],{"class":195},"free_pct",[160,3000,3001],{"class":191},":.1f",[160,3003,1340],{"class":1242},[160,3005,3006],{"class":169},"% free — prune before publishing\"",[160,3008,256],{"class":195},[145,3010,3012],{"id":3011},"conclusion","Conclusion",[10,3014,3015,3016,3018,3019,3021],{},"A network share is a perfectly good report destination as long as you treat it like the shared, remote, occasionally locked resource it is. Write to a temporary name inside the destination folder and ",[157,3017,321],{}," it into place, so nobody ever opens a half-copied workbook. Detect the Excel lock and say who holds it instead of dying on a bare ",[157,3020,910],{},". Address the share by UNC path so the scheduler sees the same thing you do, prove it is really mounted with a marker file before doing any work, and prune dated reports on a schedule with a dry run by default.",[145,3023,3025],{"id":3024},"frequently-asked-questions","Frequently asked questions",[10,3027,3028,3031],{},[300,3029,3030],{},"Why does my write fail with permission denied when the file exists?","\nSomebody has it open in Excel, which holds a lock on Windows. Write to a temporary name in the same folder and rename it into place — and even that fails while the file is open, so detect the lock and report it rather than silently skipping the publish.",[10,3033,3034,3040],{},[300,3035,3036,3037,3039],{},"Does ",[157,3038,321],{}," really overwrite atomically?","\nWithin one filesystem, yes — on both Windows and POSIX it is a single operation, so readers see either the old file or the new one. Across filesystems it degrades to a copy, so keep the temporary file in the destination folder.",[10,3042,3043,3046,3047,3050,3051,3054],{},[300,3044,3045],{},"How do I use a UNC path from Python?","\nPass it directly as a raw string, for example ",[157,3048,3049],{},"r\"\\\\server\\reports\\monthly\"",". ",[157,3052,3053],{},"pathlib"," handles UNC paths on Windows. On Linux, mount the share with cifs and use the mount point as an ordinary path.",[10,3056,3057,3060],{},[300,3058,3059],{},"Why does the script work interactively but fail under Task Scheduler?","\nThe scheduled task runs as a different account, and mapped drive letters are per-session. Use the full UNC path rather than a drive letter, and run the task as an account with rights to the share.",[10,3062,3063,3072],{},[300,3064,3065,3066,3068,3069,3071],{},"What is the ",[157,3067,2633],{}," or ",[157,3070,2629],{}," file next to my report?","\nA lock file created by Excel or LibreOffice while the document is open. Ignore it when listing reports, and treat its presence as a signal that the file is in use.",[145,3074,3076],{"id":3075},"related","Related",[3078,3079,3080,3087,3094,3100,3107],"ul",{},[3081,3082,3083,3084,3086],"li",{},"Up to the parent: ",[14,3085,17],{"href":16}," — the naming and atomicity patterns applied here.",[3081,3088,3089,3093],{},[14,3090,3092],{"href":3091},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle Permission Denied When Writing Excel in Python"," — the locked-file error in depth.",[3081,3095,3096,3099],{},[14,3097,3098],{"href":310},"Run a Python Excel Script on Windows Task Scheduler"," — why drive letters vanish under the scheduler.",[3081,3101,3102,3106],{},[14,3103,3105],{"href":3104},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002F","Upload an Excel Report to Amazon S3 with boto3"," — the same job against object storage.",[3081,3108,3109,3113],{},[14,3110,3112],{"href":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — recording what the publish step did.",[3115,3116,3117],"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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":155,"searchDepth":205,"depth":205,"links":3119},[3120,3121,3122,3123,3124,3125,3126,3127,3128,3129],{"id":147,"depth":205,"text":148},{"id":314,"depth":205,"text":315},{"id":903,"depth":205,"text":904},{"id":1679,"depth":205,"text":1680},{"id":1979,"depth":205,"text":1980},{"id":2529,"depth":205,"text":2530},{"id":2670,"depth":205,"text":2671},{"id":3011,"depth":205,"text":3012},{"id":3024,"depth":205,"text":3025},{"id":3075,"depth":205,"text":3076},"2026-08-15","Publish reports to an SMB or mounted network share safely — atomic write-then-rename, handling a file open in Excel, UNC paths, permissions, and retention of old reports.","md",[3134,3136,3139,3141,3143],{"q":3030,"a":3135},"Somebody has it open in Excel, which holds a lock on Windows. Write to a temporary name in the same folder and rename it into place — and even that fails while the file is open, so detect the lock and report it rather than silently skipping the publish.",{"q":3137,"a":3138},"Does os.replace really overwrite atomically?","Within one filesystem, yes — on both Windows and POSIX it is a single operation, so readers see either the old file or the new one. Across filesystems it degrades to a copy, so keep the temporary file in the destination folder.",{"q":3045,"a":3140},"Pass it directly as a raw string, for example r\"\\\\\\\\server\\\\reports\\\\monthly\". pathlib handles UNC paths on Windows. On Linux, mount the share with cifs and use the mount point as an ordinary path.",{"q":3059,"a":3142},"The scheduled task runs as a different account, and mapped drive letters are per-session. Use the full UNC path rather than a drive letter, and run the task as an account with rights to the share.",{"q":3144,"a":3145},"What is the .~lock or owner file next to my report?","A lock file created by Excel or LibreOffice while the document is open. Ignore it when listing reports, and treat its presence as a signal that the file is in use.",{},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python",{"title":3149,"description":3150},"Write Excel Reports to a Network Share with Python","Deliver .xlsx reports to a shared drive from a scheduled job: atomic renames, lock files left by Excel, UNC and mount paths, permission errors under cron, and retention.","write-excel-files-to-a-network-share-from-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python\u002Findex","how-to","ENK-Rpkg4yoRakAkebD9wvNNehvVw-TlRYXGhHBc_4A",[3156,3160],{"title":3157,"path":3158,"stem":3159,"children":-1},"Upload an Excel Report to SharePoint with Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python\u002Findex",{"title":3161,"path":3162,"stem":3163,"children":-1},"Scheduling Python Excel Scripts with Cron","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Findex",1786800027148]