[{"data":1,"prerenderedAt":2791},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports":2761,"ancestors:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports":2769,"children:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports":2773,"related:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports":2774},{"id":4,"title":5,"body":6,"dateModified":2728,"datePublished":2728,"description":2729,"extension":2730,"faq":2731,"keyword":2740,"kind":2741,"meta":2742,"navigation":129,"path":2750,"seo":2751,"slug":2754,"stem":2755,"topics":2756,"type":2759,"__hash__":2760},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports\u002Findex.md","Name, Version and Archive Generated Excel Reports",{"type":7,"value":8,"toc":2716},"minimark",[9,32,37,71,86,90,93,491,521,674,678,693,1023,1035,1039,1042,1620,1634,1760,1764,1767,2193,2220,2224,2235,2508,2515,2593,2597,2640,2644,2651,2655,2661,2667,2673,2679,2683,2712],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"A report job that runs every day for a year leaves a trail: dozens of files called ",[14,15,16],"code",{},"sales_report.xlsx",", ",[14,19,20],{},"sales_report (1).xlsx"," and ",[14,23,24],{},"sales_report_final_v2.xlsx",", or a single file overwritten every morning with no way back to what was sent last Tuesday. Both are problems the first time someone asks \"which numbers did the board see?\". A little structure fixes it: a naming scheme that sorts correctly and says what each file contains, a version number that only increases when the content actually changes, a manifest recording every version, and a retention rule that archives old periods without deleting the versions that matter. This page builds those pieces for a folder or network share, then maps them onto Amazon S3. It belongs to ",[27,28,30],"a",{"href":29},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl boto3 moto\n","bash","",[14,45,46],{"__ignoreMap":43},[47,48,51,55,59,62,65,68],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sMTad","pip",[47,56,58],{"class":57},"srMev"," install",[47,60,61],{"class":57}," pandas",[47,63,64],{"class":57}," openpyxl",[47,66,67],{"class":57}," boto3",[47,69,70],{"class":57}," moto\n",[10,72,73,74,77,78,81,82,85],{},"The examples write reports into a local ",[14,75,76],{},"reports\u002F"," folder; the same code works on a mounted network share. The S3 section uses ",[14,79,80],{},"boto3",", with ",[14,83,84],{},"moto"," standing in for AWS so it can run without an account.",[33,87,89],{"id":88},"step-1-a-naming-scheme-that-sorts-and-parses","Step 1 — A naming scheme that sorts and parses",[10,91,92],{},"A good file name answers three questions — which report, for which period, which version — and sorts in a file browser in the order people expect. ISO dates and zero-padded numbers do that:",[38,94,98],{"className":95,"code":96,"language":97,"meta":43,"style":43},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\nfrom pathlib import Path\n\nNAME = re.compile(r\"^(?P\u003Creport>[a-z0-9-]+)_(?P\u003Cperiod>\\d{4}-\\d{2}(?:-\\d{2})?)_v(?P\u003Cversion>\\d{3})\\.xlsx$\")\n\ndef report_name(report, period, version):\n    return f\"{report}_{period}_v{version:03d}.xlsx\"\n\ndef parse_name(name):\n    m = NAME.match(name)\n    return None if m is None else (m[\"report\"], m[\"period\"], int(m[\"version\"]))\n\nnames = [report_name(\"sales-report\", p, v) for p, v in [(\"2026-09\", 2), (\"2026-10\", 1), (\"2026-09\", 10), (\"2026-09\", 1)]]\nprint(sorted(names))\nprint(parse_name(\"sales-report_2026-09_v010.xlsx\"), parse_name(\"Sales Report final.xlsx\"))\n","python",[14,99,100,110,124,131,242,247,260,304,309,320,335,384,389,457,471],{"__ignoreMap":43},[47,101,102,106],{"class":49,"line":50},[47,103,105],{"class":104},"s-kum","import",[47,107,109],{"class":108},"skGVy"," re\n",[47,111,113,116,119,121],{"class":49,"line":112},2,[47,114,115],{"class":104},"from",[47,117,118],{"class":108}," pathlib ",[47,120,105],{"class":104},[47,122,123],{"class":108}," Path\n",[47,125,127],{"class":49,"line":126},3,[47,128,130],{"emptyLinePlaceholder":129},true,"\n",[47,132,134,138,141,144,147,150,153,157,160,164,167,170,173,176,179,182,185,188,191,193,196,199,201,203,205,207,210,212,215,217,220,222,225,227,231,234,237,239],{"class":49,"line":133},4,[47,135,137],{"class":136},"sP0c6","NAME",[47,139,140],{"class":104}," =",[47,142,143],{"class":108}," re.compile(",[47,145,146],{"class":104},"r",[47,148,149],{"class":57},"\"",[47,151,152],{"class":136},"^(",[47,154,156],{"class":155},"sx1sP","?P\u003Creport>",[47,158,159],{"class":136},"[",[47,161,163],{"class":162},"sSjpA","a-z0-9-",[47,165,166],{"class":136},"]",[47,168,169],{"class":104},"+",[47,171,172],{"class":136},")",[47,174,175],{"class":57},"_",[47,177,178],{"class":136},"(",[47,180,181],{"class":155},"?P\u003Cperiod>",[47,183,184],{"class":136},"\\d",[47,186,187],{"class":104},"{4}",[47,189,190],{"class":57},"-",[47,192,184],{"class":136},[47,194,195],{"class":104},"{2}",[47,197,198],{"class":136},"(?:",[47,200,190],{"class":57},[47,202,184],{"class":136},[47,204,195],{"class":104},[47,206,172],{"class":136},[47,208,209],{"class":104},"?",[47,211,172],{"class":136},[47,213,214],{"class":57},"_v",[47,216,178],{"class":136},[47,218,219],{"class":155},"?P\u003Cversion>",[47,221,184],{"class":136},[47,223,224],{"class":104},"{3}",[47,226,172],{"class":136},[47,228,230],{"class":229},"s_b0D","\\.",[47,232,233],{"class":57},"xlsx",[47,235,236],{"class":136},"$",[47,238,149],{"class":57},[47,240,241],{"class":108},")\n",[47,243,245],{"class":49,"line":244},5,[47,246,130],{"emptyLinePlaceholder":129},[47,248,250,253,257],{"class":49,"line":249},6,[47,251,252],{"class":104},"def",[47,254,256],{"class":255},"s_Opv"," report_name",[47,258,259],{"class":108},"(report, period, version):\n",[47,261,263,266,269,271,274,277,280,282,284,287,289,291,293,296,299,301],{"class":49,"line":262},7,[47,264,265],{"class":104},"    return",[47,267,268],{"class":104}," f",[47,270,149],{"class":57},[47,272,273],{"class":162},"{",[47,275,276],{"class":108},"report",[47,278,279],{"class":162},"}",[47,281,175],{"class":57},[47,283,273],{"class":162},[47,285,286],{"class":108},"period",[47,288,279],{"class":162},[47,290,214],{"class":57},[47,292,273],{"class":162},[47,294,295],{"class":108},"version",[47,297,298],{"class":104},":03d",[47,300,279],{"class":162},[47,302,303],{"class":57},".xlsx\"\n",[47,305,307],{"class":49,"line":306},8,[47,308,130],{"emptyLinePlaceholder":129},[47,310,312,314,317],{"class":49,"line":311},9,[47,313,252],{"class":104},[47,315,316],{"class":255}," parse_name",[47,318,319],{"class":108},"(name):\n",[47,321,323,326,329,332],{"class":49,"line":322},10,[47,324,325],{"class":108},"    m ",[47,327,328],{"class":104},"=",[47,330,331],{"class":136}," NAME",[47,333,334],{"class":108},".match(name)\n",[47,336,338,340,343,346,349,352,354,357,360,363,366,369,372,375,378,381],{"class":49,"line":337},11,[47,339,265],{"class":104},[47,341,342],{"class":136}," None",[47,344,345],{"class":104}," if",[47,347,348],{"class":108}," m ",[47,350,351],{"class":104},"is",[47,353,342],{"class":136},[47,355,356],{"class":104}," else",[47,358,359],{"class":108}," (m[",[47,361,362],{"class":57},"\"report\"",[47,364,365],{"class":108},"], m[",[47,367,368],{"class":57},"\"period\"",[47,370,371],{"class":108},"], ",[47,373,374],{"class":136},"int",[47,376,377],{"class":108},"(m[",[47,379,380],{"class":57},"\"version\"",[47,382,383],{"class":108},"]))\n",[47,385,387],{"class":49,"line":386},12,[47,388,130],{"emptyLinePlaceholder":129},[47,390,392,395,397,400,403,406,409,412,415,418,421,423,426,429,432,434,437,439,441,443,446,448,450,452,454],{"class":49,"line":391},13,[47,393,394],{"class":108},"names ",[47,396,328],{"class":104},[47,398,399],{"class":108}," [report_name(",[47,401,402],{"class":57},"\"sales-report\"",[47,404,405],{"class":108},", p, v) ",[47,407,408],{"class":104},"for",[47,410,411],{"class":108}," p, v ",[47,413,414],{"class":104},"in",[47,416,417],{"class":108}," [(",[47,419,420],{"class":57},"\"2026-09\"",[47,422,17],{"class":108},[47,424,425],{"class":136},"2",[47,427,428],{"class":108},"), (",[47,430,431],{"class":57},"\"2026-10\"",[47,433,17],{"class":108},[47,435,436],{"class":136},"1",[47,438,428],{"class":108},[47,440,420],{"class":57},[47,442,17],{"class":108},[47,444,445],{"class":136},"10",[47,447,428],{"class":108},[47,449,420],{"class":57},[47,451,17],{"class":108},[47,453,436],{"class":136},[47,455,456],{"class":108},")]]\n",[47,458,460,463,465,468],{"class":49,"line":459},14,[47,461,462],{"class":136},"print",[47,464,178],{"class":108},[47,466,467],{"class":136},"sorted",[47,469,470],{"class":108},"(names))\n",[47,472,474,476,479,482,485,488],{"class":49,"line":473},15,[47,475,462],{"class":136},[47,477,478],{"class":108},"(parse_name(",[47,480,481],{"class":57},"\"sales-report_2026-09_v010.xlsx\"",[47,483,484],{"class":108},"), parse_name(",[47,486,487],{"class":57},"\"Sales Report final.xlsx\"",[47,489,490],{"class":108},"))\n",[10,492,493,494,497,498,501,502,505,506,509,510,21,513,516,517,520],{},"The report part is lowercase with hyphens, the period is ",[14,495,496],{},"YYYY-MM"," (or ",[14,499,500],{},"YYYY-MM-DD"," for daily reports), and the version is three digits. Sorted alphabetically, the files fall into period order and then version order — ",[14,503,504],{},"v002"," before ",[14,507,508],{},"v010",", which would not happen with ",[14,511,512],{},"v2",[14,514,515],{},"v10",". Underscores separate the parts and appear nowhere else, so the name parses back unambiguously, and anything that does not match — a hand-saved copy, say — is easy to detect. Avoid spaces and characters such as ",[14,518,519],{},":"," that are illegal on Windows shares or need escaping in URLs.",[522,523,531,532,531,536,531,540,531,548,531,558,531,569,531,576,531,581,531,585,531,590,531,595,531,600,531,607,531,612,531,615,531,618,531,622,531,625,531,630,531,637,531,643,531,647,531,651,531,654,531,659,531,666,531,671],"svg",{"viewBox":524,"role":525,"ariaLabelledBy":526,"xmlns":529,"style":530},"4 0 752 142","img",[527,528],"nv-name-t","nv-name-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  ",[533,534,535],"title",{"id":527},"The parts of a report file name",[537,538,539],"desc",{"id":528},"A file name such as sales-report underscore 2026-09 underscore v003 dot xlsx holds the report name in lowercase with hyphens, the ISO period, and a zero-padded version number, separated by underscores so it sorts and parses reliably.",[541,542],"rect",{"x":543,"y":544,"width":545,"height":546,"fill":547},"4","0","752","142","#ffffff",[541,549],{"x":550,"y":551,"width":552,"height":553,"rx":554,"fill":555,"stroke":556,"style":557},"20.0","16.0","153.0","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[559,560,562],"g",{"transform":561},"translate(84.5,29.0) scale(1.000)",[563,564],"path",{"d":565,"fill":566,"stroke":567,"style":568},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[570,571,575],"text",{"x":572,"y":573,"style":574},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","sales-report",[570,577,580],{"x":572,"y":578,"style":579},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","report,",[570,582,584],{"x":572,"y":583,"style":579},"112.5","lowercase-hyphen",[49,586],{"x1":587,"y1":588,"x2":589,"y2":588,"stroke":556,"style":557},"178.0","71.2","200.0",[591,592],"polygon",{"points":593,"fill":594},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[541,596],{"x":597,"y":551,"width":552,"height":553,"rx":554,"fill":598,"stroke":599,"style":557},"209.0","#d9f4f1","var(--teal,#0f9488)",[559,601,603],{"transform":602},"translate(273.5,29.0) scale(1.000)",[563,604],{"d":605,"fill":566,"stroke":606,"style":568},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--teal-ink,#0b6157)",[570,608,611],{"x":609,"y":573,"style":610},"285.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","2026-09",[570,613,614],{"x":609,"y":578,"style":579},"ISO period, sorts by",[570,616,617],{"x":609,"y":583,"style":579},"date",[49,619],{"x1":620,"y1":588,"x2":621,"y2":588,"stroke":556,"style":557},"367.0","389.0",[591,623],{"points":624,"fill":594},"393.0,71.2 384.0,76.2 384.0,66.2",[541,626],{"x":627,"y":551,"width":552,"height":553,"rx":554,"fill":628,"stroke":629,"style":557},"398.0","#fdefd8","var(--gold,#b4740a)",[559,631,633],{"transform":632},"translate(462.5,37.8) scale(1.000)",[563,634],{"d":635,"fill":566,"stroke":636,"style":568},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--gold-ink,#7a4e06)",[570,638,642],{"x":639,"y":640,"style":641},"474.5","84.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","v003",[570,644,646],{"x":639,"y":645,"style":579},"103.8","zero-padded version",[49,648],{"x1":649,"y1":588,"x2":650,"y2":588,"stroke":556,"style":557},"556.0","578.0",[591,652],{"points":653,"fill":594},"582.0,71.2 573.0,76.2 573.0,66.2",[541,655],{"x":656,"y":551,"width":552,"height":553,"rx":554,"fill":657,"stroke":658,"style":557},"587.0","#e7ebef","var(--line,#cdd5e6)",[559,660,662],{"transform":661},"translate(651.5,37.8) scale(1.000)",[563,663],{"d":664,"fill":566,"stroke":665,"style":568},"M5 12.5l4.5 4.5L19 7","var(--text,#172033)",[570,667,670],{"x":668,"y":640,"style":669},"663.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",".xlsx",[570,672,673],{"x":668,"y":645,"style":579},"no spaces or colons",[33,675,677],{"id":676},"step-2-fingerprint-the-content-not-the-file","Step 2 — Fingerprint the content, not the file",[10,679,680,681,685,686,688,689,692],{},"To decide whether a rerun needs a new version, compare ",[682,683,684],"em",{},"content",". Hashing the file does not work, because every ",[14,687,670],{}," carries its creation and modification time in ",[14,690,691],{},"docProps\u002Fcore.xml",", so two runs with identical data produce different bytes:",[38,694,696],{"className":95,"code":695,"language":97,"meta":43,"style":43},"import hashlib\nimport time\nimport zipfile\nimport pandas as pd\n\ndef write_report(path, df):\n    df.to_excel(path, index=False, sheet_name=\"Sales\")\n\ndef file_hash(path):\n    return hashlib.sha256(Path(path).read_bytes()).hexdigest()[:12]\n\ndef content_fingerprint(path):\n    \"\"\"Hash every part of the workbook except the document properties.\"\"\"\n    h = hashlib.sha256()\n    with zipfile.ZipFile(path) as z:\n        for name in sorted(z.namelist()):\n            if not name.startswith(\"docProps\u002F\"):\n                h.update(name.encode())\n                h.update(z.read(name))\n    return h.hexdigest()[:12]\n\ndata = pd.DataFrame({\"Region\": [\"North\", \"South\"], \"Revenue\": [18400, 12100]})\nwrite_report(\"run1.xlsx\", data)\ntime.sleep(1.1)\nwrite_report(\"run2.xlsx\", data)\nprint(\"file hash:  \", file_hash(\"run1.xlsx\"), file_hash(\"run2.xlsx\"))\nprint(\"fingerprint:\", content_fingerprint(\"run1.xlsx\"), content_fingerprint(\"run2.xlsx\"))\n",[14,697,698,705,712,719,732,736,746,772,776,786,798,802,811,816,826,839,856,874,880,886,898,903,946,958,969,979,1001],{"__ignoreMap":43},[47,699,700,702],{"class":49,"line":50},[47,701,105],{"class":104},[47,703,704],{"class":108}," hashlib\n",[47,706,707,709],{"class":49,"line":112},[47,708,105],{"class":104},[47,710,711],{"class":108}," time\n",[47,713,714,716],{"class":49,"line":126},[47,715,105],{"class":104},[47,717,718],{"class":108}," zipfile\n",[47,720,721,723,726,729],{"class":49,"line":133},[47,722,105],{"class":104},[47,724,725],{"class":108}," pandas ",[47,727,728],{"class":104},"as",[47,730,731],{"class":108}," pd\n",[47,733,734],{"class":49,"line":244},[47,735,130],{"emptyLinePlaceholder":129},[47,737,738,740,743],{"class":49,"line":249},[47,739,252],{"class":104},[47,741,742],{"class":255}," write_report",[47,744,745],{"class":108},"(path, df):\n",[47,747,748,751,755,757,760,762,765,767,770],{"class":49,"line":262},[47,749,750],{"class":108},"    df.to_excel(path, ",[47,752,754],{"class":753},"sa561","index",[47,756,328],{"class":104},[47,758,759],{"class":136},"False",[47,761,17],{"class":108},[47,763,764],{"class":753},"sheet_name",[47,766,328],{"class":104},[47,768,769],{"class":57},"\"Sales\"",[47,771,241],{"class":108},[47,773,774],{"class":49,"line":306},[47,775,130],{"emptyLinePlaceholder":129},[47,777,778,780,783],{"class":49,"line":311},[47,779,252],{"class":104},[47,781,782],{"class":255}," file_hash",[47,784,785],{"class":108},"(path):\n",[47,787,788,790,793,795],{"class":49,"line":322},[47,789,265],{"class":104},[47,791,792],{"class":108}," hashlib.sha256(Path(path).read_bytes()).hexdigest()[:",[47,794,554],{"class":136},[47,796,797],{"class":108},"]\n",[47,799,800],{"class":49,"line":337},[47,801,130],{"emptyLinePlaceholder":129},[47,803,804,806,809],{"class":49,"line":386},[47,805,252],{"class":104},[47,807,808],{"class":255}," content_fingerprint",[47,810,785],{"class":108},[47,812,813],{"class":49,"line":391},[47,814,815],{"class":57},"    \"\"\"Hash every part of the workbook except the document properties.\"\"\"\n",[47,817,818,821,823],{"class":49,"line":459},[47,819,820],{"class":108},"    h ",[47,822,328],{"class":104},[47,824,825],{"class":108}," hashlib.sha256()\n",[47,827,828,831,834,836],{"class":49,"line":473},[47,829,830],{"class":104},"    with",[47,832,833],{"class":108}," zipfile.ZipFile(path) ",[47,835,728],{"class":104},[47,837,838],{"class":108}," z:\n",[47,840,842,845,848,850,853],{"class":49,"line":841},16,[47,843,844],{"class":104},"        for",[47,846,847],{"class":108}," name ",[47,849,414],{"class":104},[47,851,852],{"class":136}," sorted",[47,854,855],{"class":108},"(z.namelist()):\n",[47,857,859,862,865,868,871],{"class":49,"line":858},17,[47,860,861],{"class":104},"            if",[47,863,864],{"class":104}," not",[47,866,867],{"class":108}," name.startswith(",[47,869,870],{"class":57},"\"docProps\u002F\"",[47,872,873],{"class":108},"):\n",[47,875,877],{"class":49,"line":876},18,[47,878,879],{"class":108},"                h.update(name.encode())\n",[47,881,883],{"class":49,"line":882},19,[47,884,885],{"class":108},"                h.update(z.read(name))\n",[47,887,889,891,894,896],{"class":49,"line":888},20,[47,890,265],{"class":104},[47,892,893],{"class":108}," h.hexdigest()[:",[47,895,554],{"class":136},[47,897,797],{"class":108},[47,899,901],{"class":49,"line":900},21,[47,902,130],{"emptyLinePlaceholder":129},[47,904,906,909,911,914,917,920,923,925,928,930,933,935,938,940,943],{"class":49,"line":905},22,[47,907,908],{"class":108},"data ",[47,910,328],{"class":104},[47,912,913],{"class":108}," pd.DataFrame({",[47,915,916],{"class":57},"\"Region\"",[47,918,919],{"class":108},": [",[47,921,922],{"class":57},"\"North\"",[47,924,17],{"class":108},[47,926,927],{"class":57},"\"South\"",[47,929,371],{"class":108},[47,931,932],{"class":57},"\"Revenue\"",[47,934,919],{"class":108},[47,936,937],{"class":136},"18400",[47,939,17],{"class":108},[47,941,942],{"class":136},"12100",[47,944,945],{"class":108},"]})\n",[47,947,949,952,955],{"class":49,"line":948},23,[47,950,951],{"class":108},"write_report(",[47,953,954],{"class":57},"\"run1.xlsx\"",[47,956,957],{"class":108},", data)\n",[47,959,961,964,967],{"class":49,"line":960},24,[47,962,963],{"class":108},"time.sleep(",[47,965,966],{"class":136},"1.1",[47,968,241],{"class":108},[47,970,972,974,977],{"class":49,"line":971},25,[47,973,951],{"class":108},[47,975,976],{"class":57},"\"run2.xlsx\"",[47,978,957],{"class":108},[47,980,982,984,986,989,992,994,997,999],{"class":49,"line":981},26,[47,983,462],{"class":136},[47,985,178],{"class":108},[47,987,988],{"class":57},"\"file hash:  \"",[47,990,991],{"class":108},", file_hash(",[47,993,954],{"class":57},[47,995,996],{"class":108},"), file_hash(",[47,998,976],{"class":57},[47,1000,490],{"class":108},[47,1002,1004,1006,1008,1011,1014,1016,1019,1021],{"class":49,"line":1003},27,[47,1005,462],{"class":136},[47,1007,178],{"class":108},[47,1009,1010],{"class":57},"\"fingerprint:\"",[47,1012,1013],{"class":108},", content_fingerprint(",[47,1015,954],{"class":57},[47,1017,1018],{"class":108},"), content_fingerprint(",[47,1020,976],{"class":57},[47,1022,490],{"class":108},[10,1024,1025,1026,1029,1030,1034],{},"The file hashes differ even though nothing in the report changed; the fingerprints match. The fingerprint covers the sheets, styles, shared strings and everything else in the package except ",[14,1027,1028],{},"docProps\u002F",", so a changed number, a new row or a formatting change all produce a new fingerprint. An alternative is to make the output byte-for-byte reproducible by fixing the timestamps, as in ",[27,1031,1033],{"href":1032},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fmake-excel-output-reproducible\u002F","Make Excel Output Reproducible","; the fingerprint approach needs no change to how the workbook is written.",[33,1036,1038],{"id":1037},"step-3-publish-a-new-version-only-when-content-changes","Step 3 — Publish a new version only when content changes",[10,1040,1041],{},"The publishing function finds the latest version for the period, compares fingerprints, and either skips or writes the next version — never overwriting an existing file — and records it in a manifest:",[38,1043,1045],{"className":95,"code":1044,"language":97,"meta":43,"style":43},"import csv\nimport shutil\nfrom datetime import datetime, timezone\n\nROOT = Path(\"reports\")\nMANIFEST = ROOT \u002F \"manifest.csv\"\n\ndef versions(report, period):\n    found = [parse_name(p.name) for p in ROOT.glob(f\"{report}_{period}_v*.xlsx\")]\n    return sorted(v for (_, _, v) in filter(None, found))\n\ndef publish(tmp_file, report, period):\n    ROOT.mkdir(exist_ok=True)\n    existing = versions(report, period)\n    fp = content_fingerprint(tmp_file)\n    if existing:\n        latest = ROOT \u002F report_name(report, period, existing[-1])\n        if content_fingerprint(latest) == fp:\n            return latest, \"unchanged\"\n    target = ROOT \u002F report_name(report, period, (existing[-1] + 1) if existing else 1)\n    shutil.copy2(tmp_file, target)\n    new = not MANIFEST.exists()\n    with MANIFEST.open(\"a\", newline=\"\") as fh:\n        w = csv.writer(fh)\n        if new:\n            w.writerow([\"file\", \"report\", \"period\", \"version\", \"fingerprint\", \"published_utc\"])\n        w.writerow([target.name, report, period, target.stem.rsplit(\"_v\", 1)[1], fp,\n                    datetime.now(timezone.utc).isoformat(timespec=\"seconds\")])\n    return target, \"published\"\n\nprint(publish(\"run1.xlsx\", \"sales-report\", \"2026-09\"))\nprint(publish(\"run2.xlsx\", \"sales-report\", \"2026-09\"))           # same data, rerun\ndata.loc[1, \"Revenue\"] = 12350                                    # a late correction\nwrite_report(\"run3.xlsx\", data)\nprint(publish(\"run3.xlsx\", \"sales-report\", \"2026-09\"))\nprint(MANIFEST.read_text().splitlines()[-1].split(\",\")[:5])\n",[14,1046,1047,1054,1061,1073,1077,1092,1108,1112,1122,1169,1196,1200,1210,1228,1238,1248,1256,1277,1291,1302,1344,1349,1364,1393,1403,1410,1442,1462,1479,1490,1495,1515,1539,1561,1571,1590],{"__ignoreMap":43},[47,1048,1049,1051],{"class":49,"line":50},[47,1050,105],{"class":104},[47,1052,1053],{"class":108}," csv\n",[47,1055,1056,1058],{"class":49,"line":112},[47,1057,105],{"class":104},[47,1059,1060],{"class":108}," shutil\n",[47,1062,1063,1065,1068,1070],{"class":49,"line":126},[47,1064,115],{"class":104},[47,1066,1067],{"class":108}," datetime ",[47,1069,105],{"class":104},[47,1071,1072],{"class":108}," datetime, timezone\n",[47,1074,1075],{"class":49,"line":133},[47,1076,130],{"emptyLinePlaceholder":129},[47,1078,1079,1082,1084,1087,1090],{"class":49,"line":244},[47,1080,1081],{"class":136},"ROOT",[47,1083,140],{"class":104},[47,1085,1086],{"class":108}," Path(",[47,1088,1089],{"class":57},"\"reports\"",[47,1091,241],{"class":108},[47,1093,1094,1097,1099,1102,1105],{"class":49,"line":249},[47,1095,1096],{"class":136},"MANIFEST",[47,1098,140],{"class":104},[47,1100,1101],{"class":136}," ROOT",[47,1103,1104],{"class":104}," \u002F",[47,1106,1107],{"class":57}," \"manifest.csv\"\n",[47,1109,1110],{"class":49,"line":262},[47,1111,130],{"emptyLinePlaceholder":129},[47,1113,1114,1116,1119],{"class":49,"line":306},[47,1115,252],{"class":104},[47,1117,1118],{"class":255}," versions",[47,1120,1121],{"class":108},"(report, period):\n",[47,1123,1124,1127,1129,1132,1134,1137,1139,1141,1144,1147,1149,1151,1153,1155,1157,1159,1161,1163,1166],{"class":49,"line":311},[47,1125,1126],{"class":108},"    found ",[47,1128,328],{"class":104},[47,1130,1131],{"class":108}," [parse_name(p.name) ",[47,1133,408],{"class":104},[47,1135,1136],{"class":108}," p ",[47,1138,414],{"class":104},[47,1140,1101],{"class":136},[47,1142,1143],{"class":108},".glob(",[47,1145,1146],{"class":104},"f",[47,1148,149],{"class":57},[47,1150,273],{"class":162},[47,1152,276],{"class":108},[47,1154,279],{"class":162},[47,1156,175],{"class":57},[47,1158,273],{"class":162},[47,1160,286],{"class":108},[47,1162,279],{"class":162},[47,1164,1165],{"class":57},"_v*.xlsx\"",[47,1167,1168],{"class":108},")]\n",[47,1170,1171,1173,1175,1178,1180,1183,1185,1188,1190,1193],{"class":49,"line":322},[47,1172,265],{"class":104},[47,1174,852],{"class":136},[47,1176,1177],{"class":108},"(v ",[47,1179,408],{"class":104},[47,1181,1182],{"class":108}," (_, _, v) ",[47,1184,414],{"class":104},[47,1186,1187],{"class":136}," filter",[47,1189,178],{"class":108},[47,1191,1192],{"class":136},"None",[47,1194,1195],{"class":108},", found))\n",[47,1197,1198],{"class":49,"line":337},[47,1199,130],{"emptyLinePlaceholder":129},[47,1201,1202,1204,1207],{"class":49,"line":386},[47,1203,252],{"class":104},[47,1205,1206],{"class":255}," publish",[47,1208,1209],{"class":108},"(tmp_file, report, period):\n",[47,1211,1212,1215,1218,1221,1223,1226],{"class":49,"line":391},[47,1213,1214],{"class":136},"    ROOT",[47,1216,1217],{"class":108},".mkdir(",[47,1219,1220],{"class":753},"exist_ok",[47,1222,328],{"class":104},[47,1224,1225],{"class":136},"True",[47,1227,241],{"class":108},[47,1229,1230,1233,1235],{"class":49,"line":459},[47,1231,1232],{"class":108},"    existing ",[47,1234,328],{"class":104},[47,1236,1237],{"class":108}," versions(report, period)\n",[47,1239,1240,1243,1245],{"class":49,"line":473},[47,1241,1242],{"class":108},"    fp ",[47,1244,328],{"class":104},[47,1246,1247],{"class":108}," content_fingerprint(tmp_file)\n",[47,1249,1250,1253],{"class":49,"line":841},[47,1251,1252],{"class":104},"    if",[47,1254,1255],{"class":108}," existing:\n",[47,1257,1258,1261,1263,1265,1267,1270,1272,1274],{"class":49,"line":858},[47,1259,1260],{"class":108},"        latest ",[47,1262,328],{"class":104},[47,1264,1101],{"class":136},[47,1266,1104],{"class":104},[47,1268,1269],{"class":108}," report_name(report, period, existing[",[47,1271,190],{"class":104},[47,1273,436],{"class":136},[47,1275,1276],{"class":108},"])\n",[47,1278,1279,1282,1285,1288],{"class":49,"line":876},[47,1280,1281],{"class":104},"        if",[47,1283,1284],{"class":108}," content_fingerprint(latest) ",[47,1286,1287],{"class":104},"==",[47,1289,1290],{"class":108}," fp:\n",[47,1292,1293,1296,1299],{"class":49,"line":882},[47,1294,1295],{"class":104},"            return",[47,1297,1298],{"class":108}," latest, ",[47,1300,1301],{"class":57},"\"unchanged\"\n",[47,1303,1304,1307,1309,1311,1313,1316,1318,1320,1323,1325,1328,1331,1334,1337,1340,1342],{"class":49,"line":888},[47,1305,1306],{"class":108},"    target ",[47,1308,328],{"class":104},[47,1310,1101],{"class":136},[47,1312,1104],{"class":104},[47,1314,1315],{"class":108}," report_name(report, period, (existing[",[47,1317,190],{"class":104},[47,1319,436],{"class":136},[47,1321,1322],{"class":108},"] ",[47,1324,169],{"class":104},[47,1326,1327],{"class":136}," 1",[47,1329,1330],{"class":108},") ",[47,1332,1333],{"class":104},"if",[47,1335,1336],{"class":108}," existing ",[47,1338,1339],{"class":104},"else",[47,1341,1327],{"class":136},[47,1343,241],{"class":108},[47,1345,1346],{"class":49,"line":900},[47,1347,1348],{"class":108},"    shutil.copy2(tmp_file, target)\n",[47,1350,1351,1354,1356,1358,1361],{"class":49,"line":905},[47,1352,1353],{"class":108},"    new ",[47,1355,328],{"class":104},[47,1357,864],{"class":104},[47,1359,1360],{"class":136}," MANIFEST",[47,1362,1363],{"class":108},".exists()\n",[47,1365,1366,1368,1370,1373,1376,1378,1381,1383,1386,1388,1390],{"class":49,"line":948},[47,1367,830],{"class":104},[47,1369,1360],{"class":136},[47,1371,1372],{"class":108},".open(",[47,1374,1375],{"class":57},"\"a\"",[47,1377,17],{"class":108},[47,1379,1380],{"class":753},"newline",[47,1382,328],{"class":104},[47,1384,1385],{"class":57},"\"\"",[47,1387,1330],{"class":108},[47,1389,728],{"class":104},[47,1391,1392],{"class":108}," fh:\n",[47,1394,1395,1398,1400],{"class":49,"line":960},[47,1396,1397],{"class":108},"        w ",[47,1399,328],{"class":104},[47,1401,1402],{"class":108}," csv.writer(fh)\n",[47,1404,1405,1407],{"class":49,"line":971},[47,1406,1281],{"class":104},[47,1408,1409],{"class":108}," new:\n",[47,1411,1412,1415,1418,1420,1422,1424,1426,1428,1430,1432,1435,1437,1440],{"class":49,"line":981},[47,1413,1414],{"class":108},"            w.writerow([",[47,1416,1417],{"class":57},"\"file\"",[47,1419,17],{"class":108},[47,1421,362],{"class":57},[47,1423,17],{"class":108},[47,1425,368],{"class":57},[47,1427,17],{"class":108},[47,1429,380],{"class":57},[47,1431,17],{"class":108},[47,1433,1434],{"class":57},"\"fingerprint\"",[47,1436,17],{"class":108},[47,1438,1439],{"class":57},"\"published_utc\"",[47,1441,1276],{"class":108},[47,1443,1444,1447,1450,1452,1454,1457,1459],{"class":49,"line":1003},[47,1445,1446],{"class":108},"        w.writerow([target.name, report, period, target.stem.rsplit(",[47,1448,1449],{"class":57},"\"_v\"",[47,1451,17],{"class":108},[47,1453,436],{"class":136},[47,1455,1456],{"class":108},")[",[47,1458,436],{"class":136},[47,1460,1461],{"class":108},"], fp,\n",[47,1463,1465,1468,1471,1473,1476],{"class":49,"line":1464},28,[47,1466,1467],{"class":108},"                    datetime.now(timezone.utc).isoformat(",[47,1469,1470],{"class":753},"timespec",[47,1472,328],{"class":104},[47,1474,1475],{"class":57},"\"seconds\"",[47,1477,1478],{"class":108},")])\n",[47,1480,1482,1484,1487],{"class":49,"line":1481},29,[47,1483,265],{"class":104},[47,1485,1486],{"class":108}," target, ",[47,1488,1489],{"class":57},"\"published\"\n",[47,1491,1493],{"class":49,"line":1492},30,[47,1494,130],{"emptyLinePlaceholder":129},[47,1496,1498,1500,1503,1505,1507,1509,1511,1513],{"class":49,"line":1497},31,[47,1499,462],{"class":136},[47,1501,1502],{"class":108},"(publish(",[47,1504,954],{"class":57},[47,1506,17],{"class":108},[47,1508,402],{"class":57},[47,1510,17],{"class":108},[47,1512,420],{"class":57},[47,1514,490],{"class":108},[47,1516,1518,1520,1522,1524,1526,1528,1530,1532,1535],{"class":49,"line":1517},32,[47,1519,462],{"class":136},[47,1521,1502],{"class":108},[47,1523,976],{"class":57},[47,1525,17],{"class":108},[47,1527,402],{"class":57},[47,1529,17],{"class":108},[47,1531,420],{"class":57},[47,1533,1534],{"class":108},"))           ",[47,1536,1538],{"class":1537},"s-wDw","# same data, rerun\n",[47,1540,1542,1545,1547,1549,1551,1553,1555,1558],{"class":49,"line":1541},33,[47,1543,1544],{"class":108},"data.loc[",[47,1546,436],{"class":136},[47,1548,17],{"class":108},[47,1550,932],{"class":57},[47,1552,1322],{"class":108},[47,1554,328],{"class":104},[47,1556,1557],{"class":136}," 12350",[47,1559,1560],{"class":1537},"                                    # a late correction\n",[47,1562,1564,1566,1569],{"class":49,"line":1563},34,[47,1565,951],{"class":108},[47,1567,1568],{"class":57},"\"run3.xlsx\"",[47,1570,957],{"class":108},[47,1572,1574,1576,1578,1580,1582,1584,1586,1588],{"class":49,"line":1573},35,[47,1575,462],{"class":136},[47,1577,1502],{"class":108},[47,1579,1568],{"class":57},[47,1581,17],{"class":108},[47,1583,402],{"class":57},[47,1585,17],{"class":108},[47,1587,420],{"class":57},[47,1589,490],{"class":108},[47,1591,1593,1595,1597,1599,1602,1604,1606,1609,1612,1615,1618],{"class":49,"line":1592},36,[47,1594,462],{"class":136},[47,1596,178],{"class":108},[47,1598,1096],{"class":136},[47,1600,1601],{"class":108},".read_text().splitlines()[",[47,1603,190],{"class":104},[47,1605,436],{"class":136},[47,1607,1608],{"class":108},"].split(",[47,1610,1611],{"class":57},"\",\"",[47,1613,1614],{"class":108},")[:",[47,1616,1617],{"class":136},"5",[47,1619,1276],{"class":108},[10,1621,1622,1623,1626,1627,1629,1630,1633],{},"The first run publishes ",[14,1624,1625],{},"v001",". The rerun with identical data is recognised as unchanged and writes nothing, so a job that runs hourly does not produce 24 identical versions a day. The late correction to South's revenue produces ",[14,1628,504],{},". Every published file appears in the manifest with its fingerprint and UTC time, which answers \"which version existed when?\" without relying on file modification times that copies and backups change. If people need a stable link, also copy the newest file to a fixed name such as ",[14,1631,1632],{},"sales-report_2026-09_latest.xlsx"," — but keep the numbered files as the record.",[522,1635,531,1640,531,1643,531,1646,531,1649,531,1653,531,1659,531,1663,531,1667,531,1671,531,1674,531,1677,531,1684,531,1689,531,1694,531,1699,531,1703,531,1707,531,1709,531,1712,531,1716,531,1719,531,1722,531,1727,531,1730,531,1733,531,1735,531,1738,531,1742,531,1745,531,1748,531,1754,531,1757],{"viewBox":1636,"role":525,"ariaLabelledBy":1637,"xmlns":529,"style":530},"4 0 752 272",[1638,1639],"nv-publish-t","nv-publish-d",[533,1641,1642],{"id":1638},"What happens when the job produces a report",[537,1644,1645],{"id":1639},"The new file's content fingerprint is compared with the latest published version for the same period. If there is no version yet, it becomes v001. If the fingerprint matches, nothing is written. If it differs, the next version number is written and recorded in the manifest.",[541,1647],{"x":543,"y":544,"width":545,"height":1648,"fill":547},"272",[541,1650],{"x":1651,"y":551,"width":1652,"height":573,"rx":554,"fill":555,"stroke":556,"style":557},"239.9","280.2",[559,1654,1656],{"transform":1655},"translate(368.0,29.0) scale(1.000)",[563,1657],{"d":1658,"fill":566,"stroke":567,"style":568},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[570,1660,1662],{"x":1661,"y":573,"style":574},"380.0","Compare with the latest version",[49,1664],{"x1":1661,"y1":1665,"x2":1661,"y2":1666,"stroke":556,"style":557},"91.5","111.5",[49,1668],{"x1":1669,"y1":1666,"x2":1670,"y2":1666,"stroke":556,"style":557},"134.7","625.3",[49,1672],{"x1":1669,"y1":1666,"x2":1669,"y2":1673,"stroke":556,"style":557},"157.5",[591,1675],{"points":1676,"fill":594},"134.7,161.5 129.7,152.5 139.7,152.5",[541,1678],{"x":1679,"y":1680,"width":1681,"height":1682,"rx":445,"fill":547,"stroke":658,"style":1683},"83.7","121.5","101.8","21.0","stroke-width:1.2px",[570,1685,1688],{"x":1669,"y":1686,"style":1687},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","no version yet",[541,1690],{"x":550,"y":1691,"width":1692,"height":1693,"rx":554,"fill":555,"stroke":556,"style":557},"163.5","229.3","93.0",[559,1695,1697],{"transform":1696},"translate(122.7,176.5) scale(1.000)",[563,1698],{"d":565,"fill":566,"stroke":567,"style":568},[570,1700,1702],{"x":1669,"y":1701,"style":574},"223.0","publish v001",[570,1704,1706],{"x":1669,"y":1705,"style":579},"242.5","add to manifest",[49,1708],{"x1":1661,"y1":1666,"x2":1661,"y2":1673,"stroke":556,"style":557},[591,1710],{"points":1711,"fill":594},"380.0,161.5 375.0,152.5 385.0,152.5",[541,1713],{"x":1714,"y":1680,"width":1715,"height":1682,"rx":445,"fill":547,"stroke":658,"style":1683},"322.0","115.9",[570,1717,1718],{"x":1661,"y":1686,"style":1687},"same fingerprint",[541,1720],{"x":1721,"y":1691,"width":1692,"height":1693,"rx":554,"fill":657,"stroke":658,"style":557},"265.3",[559,1723,1725],{"transform":1724},"translate(368.0,176.5) scale(1.000)",[563,1726],{"d":664,"fill":566,"stroke":665,"style":568},[570,1728,1729],{"x":1661,"y":1701,"style":669},"skip",[570,1731,1732],{"x":1661,"y":1705,"style":579},"nothing written",[49,1734],{"x1":1670,"y1":1666,"x2":1670,"y2":1673,"stroke":556,"style":557},[591,1736],{"points":1737,"fill":594},"625.3,161.5 620.3,152.5 630.3,152.5",[541,1739],{"x":1740,"y":1680,"width":1741,"height":1682,"rx":445,"fill":547,"stroke":658,"style":1683},"566.3","118.0",[570,1743,1744],{"x":1670,"y":1686,"style":1687},"content changed",[541,1746],{"x":1747,"y":1691,"width":1692,"height":1693,"rx":554,"fill":598,"stroke":599,"style":557},"510.7",[559,1749,1751],{"transform":1750},"translate(613.3,176.5) scale(1.000)",[563,1752],{"d":1753,"fill":566,"stroke":606,"style":568},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[570,1755,1756],{"x":1670,"y":1701,"style":610},"publish next version",[570,1758,1759],{"x":1670,"y":1705,"style":579},"never overwrite",[33,1761,1763],{"id":1762},"step-4-archive-old-periods-and-keep-what-matters","Step 4 — Archive old periods and keep what matters",[10,1765,1766],{},"Without a retention rule, the folder grows forever. A typical rule keeps every version of recent periods (corrections still happen), and for older periods keeps only the final version, moved into an archive folder by year. Compute the plan first and apply it separately, so the rule can be reviewed in a dry run:",[38,1768,1770],{"className":95,"code":1769,"language":97,"meta":43,"style":43},"def retention_plan(report, keep_recent=3):\n    by_period = {}\n    for p in ROOT.glob(f\"{report}_*_v*.xlsx\"):\n        parsed = parse_name(p.name)\n        if parsed:\n            by_period.setdefault(parsed[1], []).append((parsed[2], p))\n    periods = sorted(by_period)\n    plan = []\n    for period in periods[:-keep_recent]:\n        files = sorted(by_period[period])\n        final = files[-1][1]\n        plan.append((\"archive\", final, ROOT \u002F \"archive\" \u002F period[:4] \u002F final.name))\n        plan += [(\"delete\", p, None) for _, p in files[:-1]]\n    return plan\n\nfor period, n in [(\"2026-05\", 2), (\"2026-06\", 1), (\"2026-07\", 3), (\"2026-08\", 1)]:\n    for v in range(1, n + 1):\n        shutil.copy2(\"run1.xlsx\", ROOT \u002F report_name(\"sales-report\", period, v))\n\nfor action, src, dst in retention_plan(\"sales-report\"):\n    print(f\"{action:8} {src.name}\" + (f\" → {dst.relative_to(ROOT)}\" if dst else \"\"))\n",[14,1771,1772,1789,1799,1827,1837,1844,1859,1871,1881,1898,1910,1931,1964,2001,2008,2012,2060,2085,2106,2110,2126],{"__ignoreMap":43},[47,1773,1774,1776,1779,1782,1784,1787],{"class":49,"line":50},[47,1775,252],{"class":104},[47,1777,1778],{"class":255}," retention_plan",[47,1780,1781],{"class":108},"(report, keep_recent",[47,1783,328],{"class":104},[47,1785,1786],{"class":136},"3",[47,1788,873],{"class":108},[47,1790,1791,1794,1796],{"class":49,"line":112},[47,1792,1793],{"class":108},"    by_period ",[47,1795,328],{"class":104},[47,1797,1798],{"class":108}," {}\n",[47,1800,1801,1804,1806,1808,1810,1812,1814,1816,1818,1820,1822,1825],{"class":49,"line":126},[47,1802,1803],{"class":104},"    for",[47,1805,1136],{"class":108},[47,1807,414],{"class":104},[47,1809,1101],{"class":136},[47,1811,1143],{"class":108},[47,1813,1146],{"class":104},[47,1815,149],{"class":57},[47,1817,273],{"class":162},[47,1819,276],{"class":108},[47,1821,279],{"class":162},[47,1823,1824],{"class":57},"_*_v*.xlsx\"",[47,1826,873],{"class":108},[47,1828,1829,1832,1834],{"class":49,"line":133},[47,1830,1831],{"class":108},"        parsed ",[47,1833,328],{"class":104},[47,1835,1836],{"class":108}," parse_name(p.name)\n",[47,1838,1839,1841],{"class":49,"line":244},[47,1840,1281],{"class":104},[47,1842,1843],{"class":108}," parsed:\n",[47,1845,1846,1849,1851,1854,1856],{"class":49,"line":249},[47,1847,1848],{"class":108},"            by_period.setdefault(parsed[",[47,1850,436],{"class":136},[47,1852,1853],{"class":108},"], []).append((parsed[",[47,1855,425],{"class":136},[47,1857,1858],{"class":108},"], p))\n",[47,1860,1861,1864,1866,1868],{"class":49,"line":262},[47,1862,1863],{"class":108},"    periods ",[47,1865,328],{"class":104},[47,1867,852],{"class":136},[47,1869,1870],{"class":108},"(by_period)\n",[47,1872,1873,1876,1878],{"class":49,"line":306},[47,1874,1875],{"class":108},"    plan ",[47,1877,328],{"class":104},[47,1879,1880],{"class":108}," []\n",[47,1882,1883,1885,1888,1890,1893,1895],{"class":49,"line":311},[47,1884,1803],{"class":104},[47,1886,1887],{"class":108}," period ",[47,1889,414],{"class":104},[47,1891,1892],{"class":108}," periods[:",[47,1894,190],{"class":104},[47,1896,1897],{"class":108},"keep_recent]:\n",[47,1899,1900,1903,1905,1907],{"class":49,"line":322},[47,1901,1902],{"class":108},"        files ",[47,1904,328],{"class":104},[47,1906,852],{"class":136},[47,1908,1909],{"class":108},"(by_period[period])\n",[47,1911,1912,1915,1917,1920,1922,1924,1927,1929],{"class":49,"line":337},[47,1913,1914],{"class":108},"        final ",[47,1916,328],{"class":104},[47,1918,1919],{"class":108}," files[",[47,1921,190],{"class":104},[47,1923,436],{"class":136},[47,1925,1926],{"class":108},"][",[47,1928,436],{"class":136},[47,1930,797],{"class":108},[47,1932,1933,1936,1939,1942,1944,1946,1949,1951,1954,1956,1958,1961],{"class":49,"line":386},[47,1934,1935],{"class":108},"        plan.append((",[47,1937,1938],{"class":57},"\"archive\"",[47,1940,1941],{"class":108},", final, ",[47,1943,1081],{"class":136},[47,1945,1104],{"class":104},[47,1947,1948],{"class":57}," \"archive\"",[47,1950,1104],{"class":104},[47,1952,1953],{"class":108}," period[:",[47,1955,543],{"class":136},[47,1957,1322],{"class":108},[47,1959,1960],{"class":104},"\u002F",[47,1962,1963],{"class":108}," final.name))\n",[47,1965,1966,1969,1972,1974,1977,1980,1982,1984,1986,1989,1991,1994,1996,1998],{"class":49,"line":391},[47,1967,1968],{"class":108},"        plan ",[47,1970,1971],{"class":104},"+=",[47,1973,417],{"class":108},[47,1975,1976],{"class":57},"\"delete\"",[47,1978,1979],{"class":108},", p, ",[47,1981,1192],{"class":136},[47,1983,1330],{"class":108},[47,1985,408],{"class":104},[47,1987,1988],{"class":108}," _, p ",[47,1990,414],{"class":104},[47,1992,1993],{"class":108}," files[:",[47,1995,190],{"class":104},[47,1997,436],{"class":136},[47,1999,2000],{"class":108},"]]\n",[47,2002,2003,2005],{"class":49,"line":459},[47,2004,265],{"class":104},[47,2006,2007],{"class":108}," plan\n",[47,2009,2010],{"class":49,"line":473},[47,2011,130],{"emptyLinePlaceholder":129},[47,2013,2014,2016,2019,2021,2023,2026,2028,2030,2032,2035,2037,2039,2041,2044,2046,2048,2050,2053,2055,2057],{"class":49,"line":841},[47,2015,408],{"class":104},[47,2017,2018],{"class":108}," period, n ",[47,2020,414],{"class":104},[47,2022,417],{"class":108},[47,2024,2025],{"class":57},"\"2026-05\"",[47,2027,17],{"class":108},[47,2029,425],{"class":136},[47,2031,428],{"class":108},[47,2033,2034],{"class":57},"\"2026-06\"",[47,2036,17],{"class":108},[47,2038,436],{"class":136},[47,2040,428],{"class":108},[47,2042,2043],{"class":57},"\"2026-07\"",[47,2045,17],{"class":108},[47,2047,1786],{"class":136},[47,2049,428],{"class":108},[47,2051,2052],{"class":57},"\"2026-08\"",[47,2054,17],{"class":108},[47,2056,436],{"class":136},[47,2058,2059],{"class":108},")]:\n",[47,2061,2062,2064,2067,2069,2072,2074,2076,2079,2081,2083],{"class":49,"line":858},[47,2063,1803],{"class":104},[47,2065,2066],{"class":108}," v ",[47,2068,414],{"class":104},[47,2070,2071],{"class":136}," range",[47,2073,178],{"class":108},[47,2075,436],{"class":136},[47,2077,2078],{"class":108},", n ",[47,2080,169],{"class":104},[47,2082,1327],{"class":136},[47,2084,873],{"class":108},[47,2086,2087,2090,2092,2094,2096,2098,2101,2103],{"class":49,"line":876},[47,2088,2089],{"class":108},"        shutil.copy2(",[47,2091,954],{"class":57},[47,2093,17],{"class":108},[47,2095,1081],{"class":136},[47,2097,1104],{"class":104},[47,2099,2100],{"class":108}," report_name(",[47,2102,402],{"class":57},[47,2104,2105],{"class":108},", period, v))\n",[47,2107,2108],{"class":49,"line":882},[47,2109,130],{"emptyLinePlaceholder":129},[47,2111,2112,2114,2117,2119,2122,2124],{"class":49,"line":888},[47,2113,408],{"class":104},[47,2115,2116],{"class":108}," action, src, dst ",[47,2118,414],{"class":104},[47,2120,2121],{"class":108}," retention_plan(",[47,2123,402],{"class":57},[47,2125,873],{"class":108},[47,2127,2128,2131,2133,2135,2137,2139,2142,2145,2147,2150,2153,2155,2157,2160,2163,2165,2168,2170,2173,2175,2177,2179,2181,2183,2186,2188,2191],{"class":49,"line":900},[47,2129,2130],{"class":136},"    print",[47,2132,178],{"class":108},[47,2134,1146],{"class":104},[47,2136,149],{"class":57},[47,2138,273],{"class":162},[47,2140,2141],{"class":108},"action",[47,2143,2144],{"class":104},":8",[47,2146,279],{"class":162},[47,2148,2149],{"class":162}," {",[47,2151,2152],{"class":108},"src.name",[47,2154,279],{"class":162},[47,2156,149],{"class":57},[47,2158,2159],{"class":104}," +",[47,2161,2162],{"class":108}," (",[47,2164,1146],{"class":104},[47,2166,2167],{"class":57},"\" → ",[47,2169,273],{"class":162},[47,2171,2172],{"class":108},"dst.relative_to(",[47,2174,1081],{"class":136},[47,2176,172],{"class":108},[47,2178,279],{"class":162},[47,2180,149],{"class":57},[47,2182,345],{"class":104},[47,2184,2185],{"class":108}," dst ",[47,2187,1339],{"class":104},[47,2189,2190],{"class":57}," \"\"",[47,2192,490],{"class":108},[10,2194,2195,2196,2198,2199,2202,2203,2205,2206,17,2209,21,2212,2215,2216,2219],{},"With five periods on disk and the three most recent kept intact, May and June are archived: May's final ",[14,2197,504],{}," moves to ",[14,2200,2201],{},"archive\u002F2026\u002F"," and its superseded ",[14,2204,1625],{}," is listed for deletion; June has only one version, which is archived. July, August and September keep every version. Applying the plan is a loop of ",[14,2207,2208],{},"mkdir(parents=True)",[14,2210,2211],{},"shutil.move",[14,2213,2214],{},"unlink"," — run it only after the dry-run output looks right, and log what it did. Many organisations have a legal retention period for financial reports; set ",[14,2217,2218],{},"keep_recent"," and the delete rule to match it, or skip deletion entirely and only archive.",[33,2221,2223],{"id":2222},"the-same-on-amazon-s3","The same on Amazon S3",[10,2225,2226,2227,2230,2231,2234],{},"On S3, the naming scheme becomes the object key — a prefix per report and year, such as ",[14,2228,2229],{},"reports\u002Fsales-report\u002F2026\u002Fsales-report_2026-09_v002.xlsx",", lets you list one report's history cheaply. S3 can also keep versions for you: with ",[682,2232,2233],{},"bucket versioning"," turned on, every upload to the same key keeps the previous object as a non-current version:",[38,2236,2238],{"className":95,"code":2237,"language":97,"meta":43,"style":43},"import boto3\nfrom moto import mock_aws\n\nwith mock_aws():\n    s3 = boto3.client(\"s3\", region_name=\"eu-west-1\")\n    s3.create_bucket(Bucket=\"acme-reports\", CreateBucketConfiguration={\"LocationConstraint\": \"eu-west-1\"})\n    s3.put_bucket_versioning(Bucket=\"acme-reports\", VersioningConfiguration={\"Status\": \"Enabled\"})\n    key = \"reports\u002Fsales-report\u002F2026\u002Fsales-report_2026-09.xlsx\"\n    for f in (\"run1.xlsx\", \"run3.xlsx\"):\n        s3.upload_file(f, \"acme-reports\", key, ExtraArgs={\"Metadata\": {\"fingerprint\": content_fingerprint(f)}})\n    vs = s3.list_object_versions(Bucket=\"acme-reports\", Prefix=key)[\"Versions\"]\n    print([(v[\"IsLatest\"], s3.head_object(Bucket=\"acme-reports\", Key=key, VersionId=v[\"VersionId\"])[\"Metadata\"][\"fingerprint\"]) for v in vs])\n",[14,2239,2240,2247,2259,2263,2271,2296,2329,2359,2369,2388,2416,2447],{"__ignoreMap":43},[47,2241,2242,2244],{"class":49,"line":50},[47,2243,105],{"class":104},[47,2245,2246],{"class":108}," boto3\n",[47,2248,2249,2251,2254,2256],{"class":49,"line":112},[47,2250,115],{"class":104},[47,2252,2253],{"class":108}," moto ",[47,2255,105],{"class":104},[47,2257,2258],{"class":108}," mock_aws\n",[47,2260,2261],{"class":49,"line":126},[47,2262,130],{"emptyLinePlaceholder":129},[47,2264,2265,2268],{"class":49,"line":133},[47,2266,2267],{"class":104},"with",[47,2269,2270],{"class":108}," mock_aws():\n",[47,2272,2273,2276,2278,2281,2284,2286,2289,2291,2294],{"class":49,"line":244},[47,2274,2275],{"class":108},"    s3 ",[47,2277,328],{"class":104},[47,2279,2280],{"class":108}," boto3.client(",[47,2282,2283],{"class":57},"\"s3\"",[47,2285,17],{"class":108},[47,2287,2288],{"class":753},"region_name",[47,2290,328],{"class":104},[47,2292,2293],{"class":57},"\"eu-west-1\"",[47,2295,241],{"class":108},[47,2297,2298,2301,2304,2306,2309,2311,2314,2316,2318,2321,2324,2326],{"class":49,"line":249},[47,2299,2300],{"class":108},"    s3.create_bucket(",[47,2302,2303],{"class":753},"Bucket",[47,2305,328],{"class":104},[47,2307,2308],{"class":57},"\"acme-reports\"",[47,2310,17],{"class":108},[47,2312,2313],{"class":753},"CreateBucketConfiguration",[47,2315,328],{"class":104},[47,2317,273],{"class":108},[47,2319,2320],{"class":57},"\"LocationConstraint\"",[47,2322,2323],{"class":108},": ",[47,2325,2293],{"class":57},[47,2327,2328],{"class":108},"})\n",[47,2330,2331,2334,2336,2338,2340,2342,2345,2347,2349,2352,2354,2357],{"class":49,"line":262},[47,2332,2333],{"class":108},"    s3.put_bucket_versioning(",[47,2335,2303],{"class":753},[47,2337,328],{"class":104},[47,2339,2308],{"class":57},[47,2341,17],{"class":108},[47,2343,2344],{"class":753},"VersioningConfiguration",[47,2346,328],{"class":104},[47,2348,273],{"class":108},[47,2350,2351],{"class":57},"\"Status\"",[47,2353,2323],{"class":108},[47,2355,2356],{"class":57},"\"Enabled\"",[47,2358,2328],{"class":108},[47,2360,2361,2364,2366],{"class":49,"line":306},[47,2362,2363],{"class":108},"    key ",[47,2365,328],{"class":104},[47,2367,2368],{"class":57}," \"reports\u002Fsales-report\u002F2026\u002Fsales-report_2026-09.xlsx\"\n",[47,2370,2371,2373,2376,2378,2380,2382,2384,2386],{"class":49,"line":311},[47,2372,1803],{"class":104},[47,2374,2375],{"class":108}," f ",[47,2377,414],{"class":104},[47,2379,2162],{"class":108},[47,2381,954],{"class":57},[47,2383,17],{"class":108},[47,2385,1568],{"class":57},[47,2387,873],{"class":108},[47,2389,2390,2393,2395,2398,2401,2403,2405,2408,2411,2413],{"class":49,"line":322},[47,2391,2392],{"class":108},"        s3.upload_file(f, ",[47,2394,2308],{"class":57},[47,2396,2397],{"class":108},", key, ",[47,2399,2400],{"class":753},"ExtraArgs",[47,2402,328],{"class":104},[47,2404,273],{"class":108},[47,2406,2407],{"class":57},"\"Metadata\"",[47,2409,2410],{"class":108},": {",[47,2412,1434],{"class":57},[47,2414,2415],{"class":108},": content_fingerprint(f)}})\n",[47,2417,2418,2421,2423,2426,2428,2430,2432,2434,2437,2439,2442,2445],{"class":49,"line":337},[47,2419,2420],{"class":108},"    vs ",[47,2422,328],{"class":104},[47,2424,2425],{"class":108}," s3.list_object_versions(",[47,2427,2303],{"class":753},[47,2429,328],{"class":104},[47,2431,2308],{"class":57},[47,2433,17],{"class":108},[47,2435,2436],{"class":753},"Prefix",[47,2438,328],{"class":104},[47,2440,2441],{"class":108},"key)[",[47,2443,2444],{"class":57},"\"Versions\"",[47,2446,797],{"class":108},[47,2448,2449,2451,2454,2457,2460,2462,2464,2466,2468,2471,2473,2476,2479,2481,2484,2487,2490,2492,2494,2496,2499,2501,2503,2505],{"class":49,"line":386},[47,2450,2130],{"class":136},[47,2452,2453],{"class":108},"([(v[",[47,2455,2456],{"class":57},"\"IsLatest\"",[47,2458,2459],{"class":108},"], s3.head_object(",[47,2461,2303],{"class":753},[47,2463,328],{"class":104},[47,2465,2308],{"class":57},[47,2467,17],{"class":108},[47,2469,2470],{"class":753},"Key",[47,2472,328],{"class":104},[47,2474,2475],{"class":108},"key, ",[47,2477,2478],{"class":753},"VersionId",[47,2480,328],{"class":104},[47,2482,2483],{"class":108},"v[",[47,2485,2486],{"class":57},"\"VersionId\"",[47,2488,2489],{"class":108},"])[",[47,2491,2407],{"class":57},[47,2493,1926],{"class":108},[47,2495,1434],{"class":57},[47,2497,2498],{"class":108},"]) ",[47,2500,408],{"class":104},[47,2502,2066],{"class":108},[47,2504,414],{"class":104},[47,2506,2507],{"class":108}," vs])\n",[10,2509,2510,2511,31],{},"Both uploads are kept, the newest marked as latest, and each carries its content fingerprint as object metadata so a later run can compare before uploading. Bucket versioning protects against accidental overwrites and deletes; explicit version numbers in the key remain clearer for people browsing the bucket. Lifecycle rules then handle retention without code — for example, moving non-current versions to a cheaper storage class after 90 days. Uploading itself is covered in ",[27,2512,2514],{"href":2513},"\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",[522,2516,531,2521,531,2524,531,2527,531,2530,531,2535,531,2542,531,2548,531,2554,531,2559,531,2563,531,2567,531,2569,531,2575,531,2580,531,2584,531,2587,531,2590],{"viewBox":2517,"role":525,"ariaLabelledBy":2518,"xmlns":529,"style":530},"4 0 752 207",[2519,2520],"nv-where-t","nv-where-d",[533,2522,2523],{"id":2519},"Versioning on a file share and on S3",[537,2525,2526],{"id":2520},"On a file share, versions are explicit numbers in the file name, recorded in a manifest, with retention applied by a script. On S3 with bucket versioning, every overwrite keeps the previous object, metadata can hold the fingerprint, and lifecycle rules apply retention.",[541,2528],{"x":543,"y":544,"width":545,"height":2529,"fill":547},"207",[541,2531],{"x":550,"y":551,"width":2532,"height":2533,"rx":2534,"fill":555,"stroke":556,"style":557},"351.0","175.0","14",[559,2536,2538],{"transform":2537},"translate(182.5,29.0) scale(1.083)",[563,2539],{"d":2540,"fill":566,"stroke":567,"style":2541},"M3 6h6l2 2h10v11H3z","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[570,2543,2547],{"x":2544,"y":2545,"style":2546},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","folder or network share",[49,2549],{"x1":2550,"y1":2551,"x2":2552,"y2":2551,"stroke":556,"style":2553},"36.0","91.0","355.0","stroke-width:1px",[570,2555,2558],{"x":2544,"y":2556,"style":2557},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","version number in the name",[570,2560,2562],{"x":2544,"y":2561,"style":2557},"137.0","manifest.csv records each one",[570,2564,2566],{"x":2544,"y":2565,"style":2557},"163.0","retention script, dry run first",[541,2568],{"x":621,"y":551,"width":2532,"height":2533,"rx":2534,"fill":598,"stroke":599,"style":557},[559,2570,2572],{"transform":2571},"translate(551.5,29.0) scale(1.083)",[563,2573],{"d":2574,"fill":566,"stroke":606,"style":2541},"M7 18h10a4 4 0 0 0 .6-8A6 6 0 0 0 6.1 11.4 3.4 3.4 0 0 0 7 18z",[570,2576,2579],{"x":2577,"y":2545,"style":2578},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","S3 with versioning",[49,2581],{"x1":2582,"y1":2551,"x2":2583,"y2":2551,"stroke":599,"style":2553},"405.0","724.0",[570,2585,2586],{"x":2577,"y":2556,"style":2557},"every overwrite kept",[570,2588,2589],{"x":2577,"y":2561,"style":2557},"fingerprint in object metadata",[570,2591,2592],{"x":2577,"y":2565,"style":2557},"lifecycle rules for retention",[33,2594,2596],{"id":2595},"common-pitfalls","Common pitfalls",[2598,2599,2600,2612,2622,2628,2634],"ul",{},[2601,2602,2603,2607,2608,2611],"li",{},[2604,2605,2606],"strong",{},"Hashing the whole file."," Timestamps in ",[14,2609,2610],{},"docProps"," change every run; fingerprint the content parts.",[2601,2613,2614,2617,2618,2621],{},[2604,2615,2616],{},"Overwriting the published file."," Write a new version; keep a ",[14,2619,2620],{},"latest"," copy only as a convenience.",[2601,2623,2624,2627],{},[2604,2625,2626],{},"Unsortable names."," Use ISO dates and zero-padded versions.",[2601,2629,2630,2633],{},[2604,2631,2632],{},"Deleting without a dry run."," Compute the retention plan, review it, then apply it.",[2601,2635,2636,2639],{},[2604,2637,2638],{},"Retention shorter than the legal requirement."," Check how long financial reports must be kept.",[33,2641,2643],{"id":2642},"conclusion","Conclusion",[10,2645,2646,2647,2650],{},"Give generated reports names of the form ",[14,2648,2649],{},"report_YYYY-MM_v001.xlsx"," so they sort and parse, decide whether a rerun is a new version by fingerprinting the workbook's content parts rather than the file, write each new version alongside the old ones and record it in a manifest, and apply a reviewed retention plan that archives the final version of older periods. On S3, use the same names as keys and let bucket versioning and lifecycle rules add a second layer of protection.",[33,2652,2654],{"id":2653},"frequently-asked-questions","Frequently asked questions",[10,2656,2657,2660],{},[2604,2658,2659],{},"How should I name generated Excel report files?","\nUse the report name in lowercase with hyphens, an ISO period such as 2026-09, and a zero-padded version, separated by underscores, for example sales-report_2026-09_v003.xlsx. Such names sort correctly and parse back reliably.",[10,2662,2663,2666],{},[2604,2664,2665],{},"Why does the hash of my Excel file change when the data has not?","\nEvery xlsx stores its creation and modification times in docProps\u002Fcore.xml. Hash the other parts of the zip package instead to compare content.",[10,2668,2669,2672],{},[2604,2670,2671],{},"How do I avoid creating a new version on every rerun?","\nCompare the content fingerprint of the new file with the latest published version and write a new version only when they differ.",[10,2674,2675,2678],{},[2604,2676,2677],{},"How do I archive old Excel reports with Python?","\nGroup files by period, keep every version of recent periods, move the final version of older periods into an archive folder, and review the plan in a dry run before deleting superseded versions.",[33,2680,2682],{"id":2681},"related","Related",[2598,2684,2685,2691,2698,2705],{},[2601,2686,2687,2688,2690],{},"Up one level: ",[27,2689,30],{"href":29}," — where generated reports go.",[2601,2692,2693,2697],{},[27,2694,2696],{"href":2695},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python\u002F","Write Excel Files to a Network Share from Python"," — the share the folder lives on.",[2601,2699,2700,2704],{},[27,2701,2703],{"href":2702},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python\u002F","Write Excel Files Atomically in Python"," — publishing without half-written files.",[2601,2706,2707,2711],{},[27,2708,2710],{"href":2709},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fskip-a-report-when-the-input-file-has-not-changed\u002F","Skip a Report When the Input File Has Not Changed"," — avoiding the run altogether.",[2713,2714,2715],"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 .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":43,"searchDepth":112,"depth":112,"links":2717},[2718,2719,2720,2721,2722,2723,2724,2725,2726,2727],{"id":35,"depth":112,"text":36},{"id":88,"depth":112,"text":89},{"id":676,"depth":112,"text":677},{"id":1037,"depth":112,"text":1038},{"id":1762,"depth":112,"text":1763},{"id":2222,"depth":112,"text":2223},{"id":2595,"depth":112,"text":2596},{"id":2642,"depth":112,"text":2643},{"id":2653,"depth":112,"text":2654},{"id":2681,"depth":112,"text":2682},"2026-10-02","Stop overwriting report_final_v2.xlsx: a sortable naming scheme for generated Excel reports, a content fingerprint that ignores the timestamps Excel files carry so unchanged reruns are skipped, numbered versions with a manifest, a retention rule that archives old periods and keeps final versions, and the same ideas on Amazon S3 with object versioning.","md",[2732,2734,2736,2738],{"q":2659,"a":2733},"Use the report name in lowercase with hyphens, an ISO period such as 2026-09, and a zero-padded version, separated by underscores, for example sales-report_2026-09_v003.xlsx. Such names sort correctly and parse back reliably.",{"q":2665,"a":2735},"Every xlsx stores its creation and modification times in docProps\u002Fcore.xml. Hash the other parts of the zip package instead to compare content.",{"q":2671,"a":2737},"Compare the content fingerprint of the new file with the latest published version and write a new version only when they differ.",{"q":2677,"a":2739},"Group files by period, keep every version of recent periods, move the final version of older periods into an archive folder, and review the plan in a dry run before deleting superseded versions.","name version and archive generated excel reports",null,{"breadcrumb":2743},[2744,2746,2749],{"name":2745,"item":1960},"Home",{"name":2747,"item":2748},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":30,"item":29},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports",{"title":2752,"description":2753},"Name, Version and Archive Generated Excel Reports in Python","A naming convention for generated Excel files, content fingerprints that ignore docProps timestamps, numbered versions with a manifest, retention and archiving rules, and S3 object versioning.","name-version-and-archive-generated-excel-reports","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports\u002Findex",[2757,2758],"publishing","reliable-jobs","how-to","owpJ9Rg7mUchXlEK-6sgv-2n9JCJNfQ_noXttkrDs_8",[2762,2765],{"title":30,"path":2763,"stem":2764,"children":-1},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Findex",{"title":2766,"path":2767,"stem":2768,"children":-1},"Post an Excel Report to a Slack Channel with Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-a-slack-channel-with-python\u002Findex",[2770,2772],{"path":2771,"title":2747},"\u002Fautomating-reporting-workflows",{"path":2763,"title":30},[],[2775,2778,2781,2784,2787,2790],{"path":2776,"title":2777,"kind":2759},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-an-excel-file-from-sharepoint-or-onedrive-into-pandas","Read an Excel File from SharePoint or OneDrive into pandas",{"path":2779,"title":2780,"kind":2759},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fshare-an-excel-file-in-onedrive-with-a-link-using-graph","Share an Excel File in OneDrive with a Link Using Graph",{"path":2782,"title":2783,"kind":2759},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-a-download-link-instead-of-an-excel-attachment","Email a Download Link Instead of an Excel Attachment",{"path":2785,"title":2786,"kind":2759},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses","Send an Excel Report with Amazon SES",{"path":2788,"title":2789,"kind":2759},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-an-html-report-from-excel-data-with-jinja2","Generate an HTML Report from Excel Data with Jinja2",{"path":2767,"title":2766,"kind":2759},1790972138305]