[{"data":1,"prerenderedAt":2683},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3":2675},{"id":4,"title":5,"body":6,"dateModified":2651,"datePublished":2651,"description":2652,"extension":2653,"faq":2654,"meta":2666,"navigation":400,"path":2667,"seo":2668,"slug":2671,"stem":2672,"type":2673,"__hash__":2674},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002Findex.md","Upload an Excel Report to Amazon S3 with boto3",{"type":7,"value":8,"toc":2639},"minimark",[9,19,159,164,195,198,232,247,344,348,354,660,670,677,681,684,947,965,968,1199,1202,1206,1209,1470,1477,1566,1570,1573,1660,1865,1871,1875,2010,2014,2134,2140,2285,2292,2304,2365,2374,2385,2522,2530,2534,2543,2547,2560,2576,2582,2588,2594,2598,2635],[10,11,12,13,18],"p",{},"S3 is the natural home for a generated report that other systems also consume: cheap, versioned, and reachable from anything. Getting a workbook there is one boto3 call — and then four details decide whether the result is reliable. This guide covers uploading straight from memory, setting the content type so browsers treat the object as a spreadsheet, making the write atomic so readers never catch a half-uploaded file, verifying the transfer, and sharing it without opening the bucket to the world. It is the object-storage 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,47,30,57,30,64,30,69,30,74,30,80,30,85,30,93,30,99,30,104,30,108,30,112,30,116,30,121,30,126,30,129,30,132,30,136,30,140,30,146,30,151,30,154],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"-2 40 802 179","img","Upload path: pandas writes the workbook into a memory buffer, boto3 uploads it to a staging key with the xlsx content type, then a copy promotes it to the final key and the staging object is deleted.",[26,27],"s3flow-t","s3flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:802px;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},"From DataFrame to a published S3 object without touching disk",[36,37,38],"desc",{"id":27},"Four stages. pandas writes the workbook into an in-memory BytesIO buffer. boto3's upload_fileobj sends it to a staging key carrying the spreadsheetml content type. A server-side copy promotes the staging object to the final key, which is the atomic step readers observe. The staging object is then deleted. No temporary file is ever written to the local filesystem.",[40,41],"rect",{"x":42,"y":43,"width":44,"height":45,"fill":46},"-2","40","802","179","#ffffff",[40,48],{"x":49,"y":50,"width":51,"height":52,"rx":53,"fill":54,"stroke":55,"style":56},"14","82","152","72","13","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[58,59,63],"text",{"x":60,"y":61,"style":62},"90","66","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","in memory",[58,65,68],{"x":60,"y":66,"style":67},"112","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","BytesIO",[58,70,73],{"x":60,"y":71,"style":72},"133","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","no temp file",[75,76],"line",{"x1":77,"y1":78,"x2":79,"y2":78,"stroke":55,"style":56},"166","118","198",[81,82],"polygon",{"points":83,"fill":84},"206,118 194,112 194,124","#5b5cf0",[40,86],{"x":87,"y":88,"width":89,"height":90,"rx":53,"fill":91,"stroke":92,"style":56},"214","76","186","84","#fdefd8","var(--gold,#b4740a)",[58,94,98],{"x":95,"y":96,"style":97},"307","104","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","upload_fileobj",[58,100,103],{"x":95,"y":101,"style":102},"126","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","key.uploading",[58,105,107],{"x":95,"y":106,"style":72},"145","+ ContentType",[75,109],{"x1":110,"y1":78,"x2":111,"y2":78,"stroke":92,"style":56},"400","432",[81,113],{"points":114,"fill":115},"440,118 428,112 428,124","#b4740a",[40,117],{"x":118,"y":88,"width":89,"height":90,"rx":53,"fill":119,"stroke":120,"style":56},"448","#d9f4f1","var(--teal,#0f9488)",[58,122,125],{"x":123,"y":96,"style":124},"541","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","copy_object",[58,127,128],{"x":123,"y":101,"style":102},"promotes to the real key",[58,130,131],{"x":123,"y":106,"style":72},"the atomic step",[75,133],{"x1":134,"y1":78,"x2":135,"y2":78,"stroke":120,"style":56},"634","666",[81,137],{"points":138,"fill":139},"674,118 662,112 662,124","#0f9488",[40,141],{"x":142,"y":50,"width":143,"height":52,"rx":53,"fill":144,"stroke":145,"style":56},"682","102","#f0f2f5","var(--line,#cdd5e6)",[58,147,150],{"x":148,"y":66,"style":149},"733","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","delete",[58,152,153],{"x":148,"y":71,"style":72},"staging",[58,155,158],{"x":110,"y":156,"style":157},"200","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","readers see the previous version until the copy lands — never a partial file",[160,161,163],"h2",{"id":162},"prerequisites","Prerequisites",[165,166,171],"pre",{"className":167,"code":168,"language":169,"meta":170,"style":170},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install boto3 pandas xlsxwriter\n","bash","",[172,173,174],"code",{"__ignoreMap":170},[175,176,178,182,186,189,192],"span",{"class":75,"line":177},1,[175,179,181],{"class":180},"sMTad","pip",[175,183,185],{"class":184},"srMev"," install",[175,187,188],{"class":184}," boto3",[175,190,191],{"class":184}," pandas",[175,193,194],{"class":184}," xlsxwriter\n",[10,196,197],{},"Credentials should come from the environment rather than the code. On EC2, ECS or Lambda that means an attached IAM role and nothing in the script at all; elsewhere, environment variables the scheduler injects:",[165,199,201],{"className":167,"code":200,"language":169,"meta":170,"style":170},"export AWS_REGION=eu-west-1\nexport REPORT_BUCKET=acme-reports\n",[172,202,203,219],{"__ignoreMap":170},[175,204,205,209,213,216],{"class":75,"line":177},[175,206,208],{"class":207},"s-kum","export",[175,210,212],{"class":211},"skGVy"," AWS_REGION",[175,214,215],{"class":207},"=",[175,217,218],{"class":211},"eu-west-1\n",[175,220,222,224,227,229],{"class":75,"line":221},2,[175,223,208],{"class":207},[175,225,226],{"class":211}," REPORT_BUCKET",[175,228,215],{"class":207},[175,230,231],{"class":211},"acme-reports\n",[10,233,234,235,238,239,242,243,246],{},"The minimum policy the job needs is narrow. Grant ",[172,236,237],{},"PutObject",", ",[172,240,241],{},"GetObject"," and ",[172,244,245],{},"DeleteObject"," on the report prefix only — the delete is required because the atomic pattern cleans up its staging object:",[165,248,252],{"className":249,"code":250,"language":251,"meta":170,"style":170},"language-json shiki shiki-themes github-light github-dark-high-contrast","{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [{\n    \"Effect\": \"Allow\",\n    \"Action\": [\"s3:PutObject\", \"s3:GetObject\", \"s3:DeleteObject\"],\n    \"Resource\": \"arn:aws:s3:::acme-reports\u002Freports\u002F*\"\n  }]\n}\n","json",[172,253,254,259,274,283,296,321,332,338],{"__ignoreMap":170},[175,255,256],{"class":75,"line":177},[175,257,258],{"class":211},"{\n",[175,260,261,265,268,271],{"class":75,"line":221},[175,262,264],{"class":263},"sL7So","  \"Version\"",[175,266,267],{"class":211},": ",[175,269,270],{"class":184},"\"2012-10-17\"",[175,272,273],{"class":211},",\n",[175,275,277,280],{"class":75,"line":276},3,[175,278,279],{"class":263},"  \"Statement\"",[175,281,282],{"class":211},": [{\n",[175,284,286,289,291,294],{"class":75,"line":285},4,[175,287,288],{"class":263},"    \"Effect\"",[175,290,267],{"class":211},[175,292,293],{"class":184},"\"Allow\"",[175,295,273],{"class":211},[175,297,299,302,305,308,310,313,315,318],{"class":75,"line":298},5,[175,300,301],{"class":263},"    \"Action\"",[175,303,304],{"class":211},": [",[175,306,307],{"class":184},"\"s3:PutObject\"",[175,309,238],{"class":211},[175,311,312],{"class":184},"\"s3:GetObject\"",[175,314,238],{"class":211},[175,316,317],{"class":184},"\"s3:DeleteObject\"",[175,319,320],{"class":211},"],\n",[175,322,324,327,329],{"class":75,"line":323},6,[175,325,326],{"class":263},"    \"Resource\"",[175,328,267],{"class":211},[175,330,331],{"class":184},"\"arn:aws:s3:::acme-reports\u002Freports\u002F*\"\n",[175,333,335],{"class":75,"line":334},7,[175,336,337],{"class":211},"  }]\n",[175,339,341],{"class":75,"line":340},8,[175,342,343],{"class":211},"}\n",[160,345,347],{"id":346},"step-1-upload-straight-from-memory","Step 1 — Upload straight from memory",[10,349,350,351,353],{},"Building the workbook in a ",[172,352,68],{}," avoids a temporary file entirely, which matters in a container whose filesystem is read-only or ephemeral:",[165,355,359],{"className":356,"code":357,"language":358,"meta":170,"style":170},"language-python shiki shiki-themes github-light github-dark-high-contrast","import io\nimport os\nimport boto3\nimport pandas as pd\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ns3 = boto3.client(\"s3\")\nbucket = os.environ[\"REPORT_BUCKET\"]\n\ndf = pd.DataFrame({\n    \"region\": [\"North\", \"South\", \"West\"],\n    \"revenue\": [159.92, 247.50, 137.44],\n})\n\nbuffer = io.BytesIO()\nwith pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\nbuffer.seek(0)\n\ns3.upload_fileobj(\n    buffer, bucket, \"reports\u002Fregional-latest.xlsx\",\n    ExtraArgs={\"ContentType\": XLSX},\n)\nprint(\"uploaded\")\n","python",[172,360,361,369,376,383,396,402,414,418,434,451,456,467,490,513,519,524,535,561,587,598,603,609,620,641,646],{"__ignoreMap":170},[175,362,363,366],{"class":75,"line":177},[175,364,365],{"class":207},"import",[175,367,368],{"class":211}," io\n",[175,370,371,373],{"class":75,"line":221},[175,372,365],{"class":207},[175,374,375],{"class":211}," os\n",[175,377,378,380],{"class":75,"line":276},[175,379,365],{"class":207},[175,381,382],{"class":211}," boto3\n",[175,384,385,387,390,393],{"class":75,"line":285},[175,386,365],{"class":207},[175,388,389],{"class":211}," pandas ",[175,391,392],{"class":207},"as",[175,394,395],{"class":211}," pd\n",[175,397,398],{"class":75,"line":298},[175,399,401],{"emptyLinePlaceholder":400},true,"\n",[175,403,404,408,411],{"class":75,"line":323},[175,405,407],{"class":406},"sP0c6","XLSX",[175,409,410],{"class":207}," =",[175,412,413],{"class":184}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[175,415,416],{"class":75,"line":334},[175,417,401],{"emptyLinePlaceholder":400},[175,419,420,423,425,428,431],{"class":75,"line":340},[175,421,422],{"class":211},"s3 ",[175,424,215],{"class":207},[175,426,427],{"class":211}," boto3.client(",[175,429,430],{"class":184},"\"s3\"",[175,432,433],{"class":211},")\n",[175,435,437,440,442,445,448],{"class":75,"line":436},9,[175,438,439],{"class":211},"bucket ",[175,441,215],{"class":207},[175,443,444],{"class":211}," os.environ[",[175,446,447],{"class":184},"\"REPORT_BUCKET\"",[175,449,450],{"class":211},"]\n",[175,452,454],{"class":75,"line":453},10,[175,455,401],{"emptyLinePlaceholder":400},[175,457,459,462,464],{"class":75,"line":458},11,[175,460,461],{"class":211},"df ",[175,463,215],{"class":207},[175,465,466],{"class":211}," pd.DataFrame({\n",[175,468,470,473,475,478,480,483,485,488],{"class":75,"line":469},12,[175,471,472],{"class":184},"    \"region\"",[175,474,304],{"class":211},[175,476,477],{"class":184},"\"North\"",[175,479,238],{"class":211},[175,481,482],{"class":184},"\"South\"",[175,484,238],{"class":211},[175,486,487],{"class":184},"\"West\"",[175,489,320],{"class":211},[175,491,493,496,498,501,503,506,508,511],{"class":75,"line":492},13,[175,494,495],{"class":184},"    \"revenue\"",[175,497,304],{"class":211},[175,499,500],{"class":406},"159.92",[175,502,238],{"class":211},[175,504,505],{"class":406},"247.50",[175,507,238],{"class":211},[175,509,510],{"class":406},"137.44",[175,512,320],{"class":211},[175,514,516],{"class":75,"line":515},14,[175,517,518],{"class":211},"})\n",[175,520,522],{"class":75,"line":521},15,[175,523,401],{"emptyLinePlaceholder":400},[175,525,527,530,532],{"class":75,"line":526},16,[175,528,529],{"class":211},"buffer ",[175,531,215],{"class":207},[175,533,534],{"class":211}," io.BytesIO()\n",[175,536,538,541,544,548,550,553,556,558],{"class":75,"line":537},17,[175,539,540],{"class":207},"with",[175,542,543],{"class":211}," pd.ExcelWriter(buffer, ",[175,545,547],{"class":546},"sa561","engine",[175,549,215],{"class":207},[175,551,552],{"class":184},"\"xlsxwriter\"",[175,554,555],{"class":211},") ",[175,557,392],{"class":207},[175,559,560],{"class":211}," writer:\n",[175,562,564,567,570,572,575,577,580,582,585],{"class":75,"line":563},18,[175,565,566],{"class":211},"    df.to_excel(writer, ",[175,568,569],{"class":546},"sheet_name",[175,571,215],{"class":207},[175,573,574],{"class":184},"\"Summary\"",[175,576,238],{"class":211},[175,578,579],{"class":546},"index",[175,581,215],{"class":207},[175,583,584],{"class":406},"False",[175,586,433],{"class":211},[175,588,590,593,596],{"class":75,"line":589},19,[175,591,592],{"class":211},"buffer.seek(",[175,594,595],{"class":406},"0",[175,597,433],{"class":211},[175,599,601],{"class":75,"line":600},20,[175,602,401],{"emptyLinePlaceholder":400},[175,604,606],{"class":75,"line":605},21,[175,607,608],{"class":211},"s3.upload_fileobj(\n",[175,610,612,615,618],{"class":75,"line":611},22,[175,613,614],{"class":211},"    buffer, bucket, ",[175,616,617],{"class":184},"\"reports\u002Fregional-latest.xlsx\"",[175,619,273],{"class":211},[175,621,623,626,628,631,634,636,638],{"class":75,"line":622},23,[175,624,625],{"class":546},"    ExtraArgs",[175,627,215],{"class":207},[175,629,630],{"class":211},"{",[175,632,633],{"class":184},"\"ContentType\"",[175,635,267],{"class":211},[175,637,407],{"class":406},[175,639,640],{"class":211},"},\n",[175,642,644],{"class":75,"line":643},24,[175,645,433],{"class":211},[175,647,649,652,655,658],{"class":75,"line":648},25,[175,650,651],{"class":406},"print",[175,653,654],{"class":211},"(",[175,656,657],{"class":184},"\"uploaded\"",[175,659,433],{"class":211},[10,661,662,663,666,667,669],{},"Two details carry weight. The ",[172,664,665],{},"buffer.seek(0)"," rewinds the cursor after writing — without it, ",[172,668,98],{}," reads from the end and uploads zero bytes, producing an empty object with a perfectly successful response.",[10,671,672,673,676],{},"And the content type is not cosmetic. Omit it and S3 stores the object as ",[172,674,675],{},"binary\u002Foctet-stream","; a browser following a link then downloads it as a nameless blob instead of handing it to Excel. Set it once, in a constant, so it cannot drift between call sites.",[160,678,680],{"id":679},"step-2-make-the-write-atomic","Step 2 — Make the write atomic",[10,682,683],{},"A direct upload to the final key leaves a growing, partially written object visible for the whole transfer. Anyone who opens the link in that window gets a corrupt file. Stage, then promote:",[165,685,687],{"className":356,"code":686,"language":358,"meta":170,"style":170},"import io\nimport boto3\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\ns3 = boto3.client(\"s3\")\n\ndef publish(buffer, bucket, key):\n    \"\"\"Upload atomically: stage, promote with a server-side copy, clean up.\"\"\"\n    staging = f\"{key}.uploading\"\n    buffer.seek(0)\n\n    s3.upload_fileobj(buffer, bucket, staging, ExtraArgs={\"ContentType\": XLSX})\n    try:\n        s3.copy_object(\n            Bucket=bucket,\n            Key=key,\n            CopySource={\"Bucket\": bucket, \"Key\": staging},\n            ContentType=XLSX,\n            MetadataDirective=\"REPLACE\",\n        )\n    finally:\n        s3.delete_object(Bucket=bucket, Key=staging)\n\n    return f\"s3:\u002F\u002F{bucket}\u002F{key}\"\n",[172,688,689,695,701,705,713,725,729,741,746,771,780,784,804,812,817,827,837,858,869,881,886,893,914,918],{"__ignoreMap":170},[175,690,691,693],{"class":75,"line":177},[175,692,365],{"class":207},[175,694,368],{"class":211},[175,696,697,699],{"class":75,"line":221},[175,698,365],{"class":207},[175,700,382],{"class":211},[175,702,703],{"class":75,"line":276},[175,704,401],{"emptyLinePlaceholder":400},[175,706,707,709,711],{"class":75,"line":285},[175,708,407],{"class":406},[175,710,410],{"class":207},[175,712,413],{"class":184},[175,714,715,717,719,721,723],{"class":75,"line":298},[175,716,422],{"class":211},[175,718,215],{"class":207},[175,720,427],{"class":211},[175,722,430],{"class":184},[175,724,433],{"class":211},[175,726,727],{"class":75,"line":323},[175,728,401],{"emptyLinePlaceholder":400},[175,730,731,734,738],{"class":75,"line":334},[175,732,733],{"class":207},"def",[175,735,737],{"class":736},"s_Opv"," publish",[175,739,740],{"class":211},"(buffer, bucket, key):\n",[175,742,743],{"class":75,"line":340},[175,744,745],{"class":184},"    \"\"\"Upload atomically: stage, promote with a server-side copy, clean up.\"\"\"\n",[175,747,748,751,753,756,759,762,765,768],{"class":75,"line":436},[175,749,750],{"class":211},"    staging ",[175,752,215],{"class":207},[175,754,755],{"class":207}," f",[175,757,758],{"class":184},"\"",[175,760,630],{"class":761},"sSjpA",[175,763,764],{"class":211},"key",[175,766,767],{"class":761},"}",[175,769,770],{"class":184},".uploading\"\n",[175,772,773,776,778],{"class":75,"line":453},[175,774,775],{"class":211},"    buffer.seek(",[175,777,595],{"class":406},[175,779,433],{"class":211},[175,781,782],{"class":75,"line":458},[175,783,401],{"emptyLinePlaceholder":400},[175,785,786,789,792,794,796,798,800,802],{"class":75,"line":469},[175,787,788],{"class":211},"    s3.upload_fileobj(buffer, bucket, staging, ",[175,790,791],{"class":546},"ExtraArgs",[175,793,215],{"class":207},[175,795,630],{"class":211},[175,797,633],{"class":184},[175,799,267],{"class":211},[175,801,407],{"class":406},[175,803,518],{"class":211},[175,805,806,809],{"class":75,"line":492},[175,807,808],{"class":207},"    try",[175,810,811],{"class":211},":\n",[175,813,814],{"class":75,"line":515},[175,815,816],{"class":211},"        s3.copy_object(\n",[175,818,819,822,824],{"class":75,"line":521},[175,820,821],{"class":546},"            Bucket",[175,823,215],{"class":207},[175,825,826],{"class":211},"bucket,\n",[175,828,829,832,834],{"class":75,"line":526},[175,830,831],{"class":546},"            Key",[175,833,215],{"class":207},[175,835,836],{"class":211},"key,\n",[175,838,839,842,844,846,849,852,855],{"class":75,"line":537},[175,840,841],{"class":546},"            CopySource",[175,843,215],{"class":207},[175,845,630],{"class":211},[175,847,848],{"class":184},"\"Bucket\"",[175,850,851],{"class":211},": bucket, ",[175,853,854],{"class":184},"\"Key\"",[175,856,857],{"class":211},": staging},\n",[175,859,860,863,865,867],{"class":75,"line":563},[175,861,862],{"class":546},"            ContentType",[175,864,215],{"class":207},[175,866,407],{"class":406},[175,868,273],{"class":211},[175,870,871,874,876,879],{"class":75,"line":589},[175,872,873],{"class":546},"            MetadataDirective",[175,875,215],{"class":207},[175,877,878],{"class":184},"\"REPLACE\"",[175,880,273],{"class":211},[175,882,883],{"class":75,"line":600},[175,884,885],{"class":211},"        )\n",[175,887,888,891],{"class":75,"line":605},[175,889,890],{"class":207},"    finally",[175,892,811],{"class":211},[175,894,895,898,901,903,906,909,911],{"class":75,"line":611},[175,896,897],{"class":211},"        s3.delete_object(",[175,899,900],{"class":546},"Bucket",[175,902,215],{"class":207},[175,904,905],{"class":211},"bucket, ",[175,907,908],{"class":546},"Key",[175,910,215],{"class":207},[175,912,913],{"class":211},"staging)\n",[175,915,916],{"class":75,"line":622},[175,917,401],{"emptyLinePlaceholder":400},[175,919,920,923,925,928,930,933,935,938,940,942,944],{"class":75,"line":643},[175,921,922],{"class":207},"    return",[175,924,755],{"class":207},[175,926,927],{"class":184},"\"s3:\u002F\u002F",[175,929,630],{"class":761},[175,931,932],{"class":211},"bucket",[175,934,767],{"class":761},[175,936,937],{"class":184},"\u002F",[175,939,630],{"class":761},[175,941,764],{"class":211},[175,943,767],{"class":761},[175,945,946],{"class":184},"\"\n",[10,948,949,952,953,956,957,960,961,964],{},[172,950,951],{},"MetadataDirective=\"REPLACE\""," is required whenever you set ",[172,954,955],{},"ContentType"," on the copy — with the default ",[172,958,959],{},"COPY",", the argument is ignored and the promoted object inherits the source metadata. The ",[172,962,963],{},"finally"," guarantees the staging object is removed even when the copy fails, so a failed run does not leave litter that the next one trips over.",[10,966,967],{},"Write the dated key and the stable key in one function, so history and links stay in step:",[165,969,971],{"className":356,"code":970,"language":358,"meta":170,"style":170},"from datetime import date\n\ndef publish_report(buffer, bucket, name, on=None):\n    on = on or date.today()\n    dated = f\"reports\u002F{on:%Y\u002F%m}\u002F{name}-{on:%Y-%m}.xlsx\"\n    latest = f\"reports\u002F{name}-latest.xlsx\"\n\n    publish(buffer, bucket, dated)\n\n    # Server-side copy: the bytes never leave S3 for this second write.\n    s3.copy_object(\n        Bucket=bucket, Key=latest,\n        CopySource={\"Bucket\": bucket, \"Key\": dated},\n        ContentType=XLSX, MetadataDirective=\"REPLACE\",\n    )\n    return dated, latest\n",[172,972,973,986,990,1008,1024,1089,1109,1113,1118,1122,1128,1133,1149,1167,1187,1192],{"__ignoreMap":170},[175,974,975,978,981,983],{"class":75,"line":177},[175,976,977],{"class":207},"from",[175,979,980],{"class":211}," datetime ",[175,982,365],{"class":207},[175,984,985],{"class":211}," date\n",[175,987,988],{"class":75,"line":221},[175,989,401],{"emptyLinePlaceholder":400},[175,991,992,994,997,1000,1002,1005],{"class":75,"line":276},[175,993,733],{"class":207},[175,995,996],{"class":736}," publish_report",[175,998,999],{"class":211},"(buffer, bucket, name, on",[175,1001,215],{"class":207},[175,1003,1004],{"class":406},"None",[175,1006,1007],{"class":211},"):\n",[175,1009,1010,1013,1015,1018,1021],{"class":75,"line":285},[175,1011,1012],{"class":211},"    on ",[175,1014,215],{"class":207},[175,1016,1017],{"class":211}," on ",[175,1019,1020],{"class":207},"or",[175,1022,1023],{"class":211}," date.today()\n",[175,1025,1026,1029,1031,1033,1036,1038,1041,1044,1047,1050,1053,1056,1058,1060,1062,1065,1067,1070,1072,1075,1077,1079,1082,1084,1086],{"class":75,"line":298},[175,1027,1028],{"class":211},"    dated ",[175,1030,215],{"class":207},[175,1032,755],{"class":207},[175,1034,1035],{"class":184},"\"reports\u002F",[175,1037,630],{"class":761},[175,1039,1040],{"class":211},"on",[175,1042,1043],{"class":207},":",[175,1045,1046],{"class":211},"%",[175,1048,1049],{"class":207},"Y",[175,1051,1052],{"class":211},"\u002F%",[175,1054,1055],{"class":207},"m",[175,1057,767],{"class":761},[175,1059,937],{"class":184},[175,1061,630],{"class":761},[175,1063,1064],{"class":211},"name",[175,1066,767],{"class":761},[175,1068,1069],{"class":184},"-",[175,1071,630],{"class":761},[175,1073,1074],{"class":211},"on:",[175,1076,1046],{"class":207},[175,1078,1049],{"class":211},[175,1080,1081],{"class":207},"-%",[175,1083,1055],{"class":211},[175,1085,767],{"class":761},[175,1087,1088],{"class":184},".xlsx\"\n",[175,1090,1091,1094,1096,1098,1100,1102,1104,1106],{"class":75,"line":323},[175,1092,1093],{"class":211},"    latest ",[175,1095,215],{"class":207},[175,1097,755],{"class":207},[175,1099,1035],{"class":184},[175,1101,630],{"class":761},[175,1103,1064],{"class":211},[175,1105,767],{"class":761},[175,1107,1108],{"class":184},"-latest.xlsx\"\n",[175,1110,1111],{"class":75,"line":334},[175,1112,401],{"emptyLinePlaceholder":400},[175,1114,1115],{"class":75,"line":340},[175,1116,1117],{"class":211},"    publish(buffer, bucket, dated)\n",[175,1119,1120],{"class":75,"line":436},[175,1121,401],{"emptyLinePlaceholder":400},[175,1123,1124],{"class":75,"line":453},[175,1125,1127],{"class":1126},"s-wDw","    # Server-side copy: the bytes never leave S3 for this second write.\n",[175,1129,1130],{"class":75,"line":458},[175,1131,1132],{"class":211},"    s3.copy_object(\n",[175,1134,1135,1138,1140,1142,1144,1146],{"class":75,"line":469},[175,1136,1137],{"class":546},"        Bucket",[175,1139,215],{"class":207},[175,1141,905],{"class":211},[175,1143,908],{"class":546},[175,1145,215],{"class":207},[175,1147,1148],{"class":211},"latest,\n",[175,1150,1151,1154,1156,1158,1160,1162,1164],{"class":75,"line":492},[175,1152,1153],{"class":546},"        CopySource",[175,1155,215],{"class":207},[175,1157,630],{"class":211},[175,1159,848],{"class":184},[175,1161,851],{"class":211},[175,1163,854],{"class":184},[175,1165,1166],{"class":211},": dated},\n",[175,1168,1169,1172,1174,1176,1178,1181,1183,1185],{"class":75,"line":515},[175,1170,1171],{"class":546},"        ContentType",[175,1173,215],{"class":207},[175,1175,407],{"class":406},[175,1177,238],{"class":211},[175,1179,1180],{"class":546},"MetadataDirective",[175,1182,215],{"class":207},[175,1184,878],{"class":184},[175,1186,273],{"class":211},[175,1188,1189],{"class":75,"line":521},[175,1190,1191],{"class":211},"    )\n",[175,1193,1194,1196],{"class":75,"line":526},[175,1195,922],{"class":207},[175,1197,1198],{"class":211}," dated, latest\n",[10,1200,1201],{},"The second write is a server-side copy rather than a second upload — no bytes cross the network again, which on a large report is the difference between one transfer and two.",[160,1203,1205],{"id":1204},"step-3-verify-the-object","Step 3 — Verify the object",[10,1207,1208],{},"A successful call means the request was accepted. Confirm the object is what you sent:",[165,1210,1212],{"className":356,"code":1211,"language":358,"meta":170,"style":170},"import hashlib\nimport boto3\n\ns3 = boto3.client(\"s3\")\n\ndef verify(buffer, bucket, key):\n    \"\"\"Check the stored object matches the buffer that was uploaded.\"\"\"\n    buffer.seek(0)\n    payload = buffer.read()\n\n    head = s3.head_object(Bucket=bucket, Key=key)\n\n    if head[\"ContentLength\"] != len(payload):\n        raise RuntimeError(\n            f\"size mismatch: sent {len(payload)}, stored {head['ContentLength']}\"\n        )\n\n    etag = head[\"ETag\"].strip('\"')\n    if \"-\" in etag:\n        # Multipart upload: the ETag is a digest of digests, not the file MD5.\n        return \"size-verified\"\n\n    if etag != hashlib.md5(payload).hexdigest():\n        raise RuntimeError(\"checksum mismatch — the stored object is corrupt\")\n    return \"checksum-verified\"\n",[172,1213,1214,1221,1227,1231,1243,1247,1256,1261,1269,1279,1283,1306,1310,1333,1344,1380,1384,1388,1408,1421,1426,1434,1438,1450,1463],{"__ignoreMap":170},[175,1215,1216,1218],{"class":75,"line":177},[175,1217,365],{"class":207},[175,1219,1220],{"class":211}," hashlib\n",[175,1222,1223,1225],{"class":75,"line":221},[175,1224,365],{"class":207},[175,1226,382],{"class":211},[175,1228,1229],{"class":75,"line":276},[175,1230,401],{"emptyLinePlaceholder":400},[175,1232,1233,1235,1237,1239,1241],{"class":75,"line":285},[175,1234,422],{"class":211},[175,1236,215],{"class":207},[175,1238,427],{"class":211},[175,1240,430],{"class":184},[175,1242,433],{"class":211},[175,1244,1245],{"class":75,"line":298},[175,1246,401],{"emptyLinePlaceholder":400},[175,1248,1249,1251,1254],{"class":75,"line":323},[175,1250,733],{"class":207},[175,1252,1253],{"class":736}," verify",[175,1255,740],{"class":211},[175,1257,1258],{"class":75,"line":334},[175,1259,1260],{"class":184},"    \"\"\"Check the stored object matches the buffer that was uploaded.\"\"\"\n",[175,1262,1263,1265,1267],{"class":75,"line":340},[175,1264,775],{"class":211},[175,1266,595],{"class":406},[175,1268,433],{"class":211},[175,1270,1271,1274,1276],{"class":75,"line":436},[175,1272,1273],{"class":211},"    payload ",[175,1275,215],{"class":207},[175,1277,1278],{"class":211}," buffer.read()\n",[175,1280,1281],{"class":75,"line":453},[175,1282,401],{"emptyLinePlaceholder":400},[175,1284,1285,1288,1290,1293,1295,1297,1299,1301,1303],{"class":75,"line":458},[175,1286,1287],{"class":211},"    head ",[175,1289,215],{"class":207},[175,1291,1292],{"class":211}," s3.head_object(",[175,1294,900],{"class":546},[175,1296,215],{"class":207},[175,1298,905],{"class":211},[175,1300,908],{"class":546},[175,1302,215],{"class":207},[175,1304,1305],{"class":211},"key)\n",[175,1307,1308],{"class":75,"line":469},[175,1309,401],{"emptyLinePlaceholder":400},[175,1311,1312,1315,1318,1321,1324,1327,1330],{"class":75,"line":492},[175,1313,1314],{"class":207},"    if",[175,1316,1317],{"class":211}," head[",[175,1319,1320],{"class":184},"\"ContentLength\"",[175,1322,1323],{"class":211},"] ",[175,1325,1326],{"class":207},"!=",[175,1328,1329],{"class":406}," len",[175,1331,1332],{"class":211},"(payload):\n",[175,1334,1335,1338,1341],{"class":75,"line":515},[175,1336,1337],{"class":207},"        raise",[175,1339,1340],{"class":406}," RuntimeError",[175,1342,1343],{"class":211},"(\n",[175,1345,1346,1349,1352,1354,1357,1360,1362,1365,1367,1370,1373,1376,1378],{"class":75,"line":521},[175,1347,1348],{"class":207},"            f",[175,1350,1351],{"class":184},"\"size mismatch: sent ",[175,1353,630],{"class":761},[175,1355,1356],{"class":406},"len",[175,1358,1359],{"class":211},"(payload)",[175,1361,767],{"class":761},[175,1363,1364],{"class":184},", stored ",[175,1366,630],{"class":761},[175,1368,1369],{"class":211},"head[",[175,1371,1372],{"class":184},"'ContentLength'",[175,1374,1375],{"class":211},"]",[175,1377,767],{"class":761},[175,1379,946],{"class":184},[175,1381,1382],{"class":75,"line":526},[175,1383,885],{"class":211},[175,1385,1386],{"class":75,"line":537},[175,1387,401],{"emptyLinePlaceholder":400},[175,1389,1390,1393,1395,1397,1400,1403,1406],{"class":75,"line":563},[175,1391,1392],{"class":211},"    etag ",[175,1394,215],{"class":207},[175,1396,1317],{"class":211},[175,1398,1399],{"class":184},"\"ETag\"",[175,1401,1402],{"class":211},"].strip(",[175,1404,1405],{"class":184},"'\"'",[175,1407,433],{"class":211},[175,1409,1410,1412,1415,1418],{"class":75,"line":589},[175,1411,1314],{"class":207},[175,1413,1414],{"class":184}," \"-\"",[175,1416,1417],{"class":207}," in",[175,1419,1420],{"class":211}," etag:\n",[175,1422,1423],{"class":75,"line":600},[175,1424,1425],{"class":1126},"        # Multipart upload: the ETag is a digest of digests, not the file MD5.\n",[175,1427,1428,1431],{"class":75,"line":605},[175,1429,1430],{"class":207},"        return",[175,1432,1433],{"class":184}," \"size-verified\"\n",[175,1435,1436],{"class":75,"line":611},[175,1437,401],{"emptyLinePlaceholder":400},[175,1439,1440,1442,1445,1447],{"class":75,"line":622},[175,1441,1314],{"class":207},[175,1443,1444],{"class":211}," etag ",[175,1446,1326],{"class":207},[175,1448,1449],{"class":211}," hashlib.md5(payload).hexdigest():\n",[175,1451,1452,1454,1456,1458,1461],{"class":75,"line":643},[175,1453,1337],{"class":207},[175,1455,1340],{"class":406},[175,1457,654],{"class":211},[175,1459,1460],{"class":184},"\"checksum mismatch — the stored object is corrupt\"",[175,1462,433],{"class":211},[175,1464,1465,1467],{"class":75,"line":648},[175,1466,922],{"class":207},[175,1468,1469],{"class":184}," \"checksum-verified\"\n",[10,1471,1472,1473,1476],{},"The multipart branch is the part people get wrong. boto3 switches to multipart automatically above a threshold, and for those objects the ETag has a ",[172,1474,1475],{},"-N"," suffix and is not the file's MD5 — comparing it directly reports corruption on every large file. If you want a real checksum on large objects, ask S3 for one explicitly:",[165,1478,1480],{"className":356,"code":1479,"language":358,"meta":170,"style":170},"s3.upload_fileobj(\n    buffer, bucket, key,\n    ExtraArgs={\"ContentType\": XLSX, \"ChecksumAlgorithm\": \"SHA256\"},\n)\nhead = s3.head_object(Bucket=bucket, Key=key, ChecksumMode=\"ENABLED\")\nprint(head.get(\"ChecksumSHA256\"))\n",[172,1481,1482,1486,1491,1517,1521,1553],{"__ignoreMap":170},[175,1483,1484],{"class":75,"line":177},[175,1485,608],{"class":211},[175,1487,1488],{"class":75,"line":221},[175,1489,1490],{"class":211},"    buffer, bucket, key,\n",[175,1492,1493,1495,1497,1499,1501,1503,1505,1507,1510,1512,1515],{"class":75,"line":276},[175,1494,625],{"class":546},[175,1496,215],{"class":207},[175,1498,630],{"class":211},[175,1500,633],{"class":184},[175,1502,267],{"class":211},[175,1504,407],{"class":406},[175,1506,238],{"class":211},[175,1508,1509],{"class":184},"\"ChecksumAlgorithm\"",[175,1511,267],{"class":211},[175,1513,1514],{"class":184},"\"SHA256\"",[175,1516,640],{"class":211},[175,1518,1519],{"class":75,"line":285},[175,1520,433],{"class":211},[175,1522,1523,1526,1528,1530,1532,1534,1536,1538,1540,1543,1546,1548,1551],{"class":75,"line":298},[175,1524,1525],{"class":211},"head ",[175,1527,215],{"class":207},[175,1529,1292],{"class":211},[175,1531,900],{"class":546},[175,1533,215],{"class":207},[175,1535,905],{"class":211},[175,1537,908],{"class":546},[175,1539,215],{"class":207},[175,1541,1542],{"class":211},"key, ",[175,1544,1545],{"class":546},"ChecksumMode",[175,1547,215],{"class":207},[175,1549,1550],{"class":184},"\"ENABLED\"",[175,1552,433],{"class":211},[175,1554,1555,1557,1560,1563],{"class":75,"line":323},[175,1556,651],{"class":406},[175,1558,1559],{"class":211},"(head.get(",[175,1561,1562],{"class":184},"\"ChecksumSHA256\"",[175,1564,1565],{"class":211},"))\n",[160,1567,1569],{"id":1568},"step-4-share-without-a-public-bucket","Step 4 — Share without a public bucket",[10,1571,1572],{},"Presigned URLs give time-limited access to a single object using your credentials, so the bucket stays private:",[20,1574,30,1580,30,1583,30,1586,30,1590,30,1594,30,1598,30,1601,30,1606,30,1609,30,1613,30,1617,30,1620,30,1623,30,1627,30,1630,30,1633,30,1637,30,1640,30,1644,30,1647,30,1649,30,1653,30,1656],{"viewBox":1575,"role":23,"ariaLabel":1576,"ariaLabelledBy":1577,"xmlns":28,"style":29},"-2 60 802 150","Presigned URL flow: the job asks S3 to sign a URL for one object with an expiry, emails the link, and the recipient downloads directly from S3 without any credentials or public bucket access.",[1578,1579],"presign-t","presign-d",[32,1581,1582],{"id":1578},"How a presigned link reaches a reader without opening the bucket",[36,1584,1585],{"id":1579},"The reporting job calls generate_presigned_url, which signs a URL scoped to one object key with an expiry time. That link is emailed to the reader. The reader's browser fetches the object directly from S3, which validates the signature and the expiry. The bucket itself remains private throughout, and the link stops working when it expires.",[40,1587],{"x":42,"y":1588,"width":44,"height":1589,"fill":46},"60","150",[40,1591],{"x":49,"y":50,"width":1592,"height":1593,"rx":53,"fill":54,"stroke":55,"style":56},"164","70",[58,1595,1597],{"x":1596,"y":66,"style":67},"96","reporting job",[58,1599,1600],{"x":1596,"y":71,"style":72},"holds the credentials",[75,1602],{"x1":1603,"y1":1604,"x2":1605,"y2":1604,"stroke":55,"style":56},"178","117","212",[81,1607],{"points":1608,"fill":84},"220,117 208,111 208,123",[40,1610],{"x":1611,"y":88,"width":1612,"height":50,"rx":53,"fill":91,"stroke":92,"style":56},"228","188",[58,1614,1616],{"x":1615,"y":96,"style":97},"322","presigned URL",[58,1618,1619],{"x":1615,"y":101,"style":102},"one key · one expiry",[58,1621,1622],{"x":1615,"y":106,"style":72},"signed, not public",[75,1624],{"x1":1625,"y1":1604,"x2":1626,"y2":1604,"stroke":92,"style":56},"416","450",[81,1628],{"points":1629,"fill":115},"458,117 446,111 446,123",[40,1631],{"x":1632,"y":50,"width":1589,"height":1593,"rx":53,"fill":119,"stroke":120,"style":56},"466",[58,1634,1636],{"x":123,"y":66,"style":1635},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","the reader",[58,1638,1639],{"x":123,"y":71,"style":72},"no AWS account",[75,1641],{"x1":1642,"y1":1604,"x2":1643,"y2":1604,"stroke":120,"style":56},"616","650",[81,1645],{"points":1646,"fill":139},"658,117 646,111 646,123",[40,1648],{"x":135,"y":50,"width":78,"height":1593,"rx":53,"fill":144,"stroke":145,"style":56},[58,1650,1652],{"x":1651,"y":66,"style":149},"725","S3",[58,1654,1655],{"x":1651,"y":71,"style":72},"bucket private",[58,1657,1659],{"x":110,"y":1658,"style":157},"192","anyone holding the link can download until it expires — keep the window short",[165,1661,1663],{"className":356,"code":1662,"language":358,"meta":170,"style":170},"import boto3\n\ns3 = boto3.client(\"s3\")\n\ndef share_link(bucket, key, hours=48, filename=None):\n    \"\"\"Time-limited download link for one object.\"\"\"\n    params = {\"Bucket\": bucket, \"Key\": key}\n    if filename:\n        # Force a friendly filename in the browser's save dialog.\n        params[\"ResponseContentDisposition\"] = f'attachment; filename=\"{filename}\"'\n\n    return s3.generate_presigned_url(\n        \"get_object\", Params=params, ExpiresIn=hours * 3600\n    )\n\nurl = share_link(\"acme-reports\", \"reports\u002Fregional-latest.xlsx\",\n                 hours=48, filename=\"Regional report August 2026.xlsx\")\n",[172,1664,1665,1671,1675,1687,1691,1715,1720,1739,1746,1751,1778,1782,1789,1818,1822,1826,1845],{"__ignoreMap":170},[175,1666,1667,1669],{"class":75,"line":177},[175,1668,365],{"class":207},[175,1670,382],{"class":211},[175,1672,1673],{"class":75,"line":221},[175,1674,401],{"emptyLinePlaceholder":400},[175,1676,1677,1679,1681,1683,1685],{"class":75,"line":276},[175,1678,422],{"class":211},[175,1680,215],{"class":207},[175,1682,427],{"class":211},[175,1684,430],{"class":184},[175,1686,433],{"class":211},[175,1688,1689],{"class":75,"line":285},[175,1690,401],{"emptyLinePlaceholder":400},[175,1692,1693,1695,1698,1701,1703,1706,1709,1711,1713],{"class":75,"line":298},[175,1694,733],{"class":207},[175,1696,1697],{"class":736}," share_link",[175,1699,1700],{"class":211},"(bucket, key, hours",[175,1702,215],{"class":207},[175,1704,1705],{"class":406},"48",[175,1707,1708],{"class":211},", filename",[175,1710,215],{"class":207},[175,1712,1004],{"class":406},[175,1714,1007],{"class":211},[175,1716,1717],{"class":75,"line":323},[175,1718,1719],{"class":184},"    \"\"\"Time-limited download link for one object.\"\"\"\n",[175,1721,1722,1725,1727,1730,1732,1734,1736],{"class":75,"line":334},[175,1723,1724],{"class":211},"    params ",[175,1726,215],{"class":207},[175,1728,1729],{"class":211}," {",[175,1731,848],{"class":184},[175,1733,851],{"class":211},[175,1735,854],{"class":184},[175,1737,1738],{"class":211},": key}\n",[175,1740,1741,1743],{"class":75,"line":340},[175,1742,1314],{"class":207},[175,1744,1745],{"class":211}," filename:\n",[175,1747,1748],{"class":75,"line":436},[175,1749,1750],{"class":1126},"        # Force a friendly filename in the browser's save dialog.\n",[175,1752,1753,1756,1759,1761,1763,1765,1768,1770,1773,1775],{"class":75,"line":453},[175,1754,1755],{"class":211},"        params[",[175,1757,1758],{"class":184},"\"ResponseContentDisposition\"",[175,1760,1323],{"class":211},[175,1762,215],{"class":207},[175,1764,755],{"class":207},[175,1766,1767],{"class":184},"'attachment; filename=\"",[175,1769,630],{"class":761},[175,1771,1772],{"class":211},"filename",[175,1774,767],{"class":761},[175,1776,1777],{"class":184},"\"'\n",[175,1779,1780],{"class":75,"line":458},[175,1781,401],{"emptyLinePlaceholder":400},[175,1783,1784,1786],{"class":75,"line":469},[175,1785,922],{"class":207},[175,1787,1788],{"class":211}," s3.generate_presigned_url(\n",[175,1790,1791,1794,1796,1799,1801,1804,1807,1809,1812,1815],{"class":75,"line":492},[175,1792,1793],{"class":184},"        \"get_object\"",[175,1795,238],{"class":211},[175,1797,1798],{"class":546},"Params",[175,1800,215],{"class":207},[175,1802,1803],{"class":211},"params, ",[175,1805,1806],{"class":546},"ExpiresIn",[175,1808,215],{"class":207},[175,1810,1811],{"class":211},"hours ",[175,1813,1814],{"class":207},"*",[175,1816,1817],{"class":406}," 3600\n",[175,1819,1820],{"class":75,"line":515},[175,1821,1191],{"class":211},[175,1823,1824],{"class":75,"line":521},[175,1825,401],{"emptyLinePlaceholder":400},[175,1827,1828,1831,1833,1836,1839,1841,1843],{"class":75,"line":526},[175,1829,1830],{"class":211},"url ",[175,1832,215],{"class":207},[175,1834,1835],{"class":211}," share_link(",[175,1837,1838],{"class":184},"\"acme-reports\"",[175,1840,238],{"class":211},[175,1842,617],{"class":184},[175,1844,273],{"class":211},[175,1846,1847,1850,1852,1854,1856,1858,1860,1863],{"class":75,"line":537},[175,1848,1849],{"class":546},"                 hours",[175,1851,215],{"class":207},[175,1853,1705],{"class":406},[175,1855,238],{"class":211},[175,1857,1772],{"class":546},[175,1859,215],{"class":207},[175,1861,1862],{"class":184},"\"Regional report August 2026.xlsx\"",[175,1864,433],{"class":211},[10,1866,1867,1868,1870],{},"Two operational cautions. The link is a bearer token — anyone who has it can download until it expires, so keep the window short and treat it as sensitive. And a presigned URL cannot outlive the credentials that signed it; a URL signed by a role session with a one-hour lifetime stops working after that hour regardless of the ",[172,1869,1806],{}," you asked for. For long-lived links, sign with a longer-lived identity or re-sign on each run.",[160,1872,1874],{"id":1873},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1876,1877,1878,1894],"table",{},[1879,1880,1881],"thead",{},[1882,1883,1884,1888,1891],"tr",{},[1885,1886,1887],"th",{},"Symptom",[1885,1889,1890],{},"Cause",[1885,1892,1893],{},"Fix",[1895,1896,1897,1911,1924,1942,1956,1972,1985,1996],"tbody",{},[1882,1898,1899,1903,1906],{},[1900,1901,1902],"td",{},"Object is 0 bytes",[1900,1904,1905],{},"Buffer not rewound",[1900,1907,1908,1910],{},[172,1909,665],{}," before uploading.",[1882,1912,1913,1916,1919],{},[1900,1914,1915],{},"Browser downloads a nameless blob",[1900,1917,1918],{},"Content type not set",[1900,1920,1921,1922,18],{},"Pass the spreadsheetml ",[172,1923,955],{},[1882,1925,1926,1929,1936],{},[1900,1927,1928],{},"Content type lost on the copy",[1900,1930,1931,1933,1934],{},[172,1932,1180],{}," left as ",[172,1935,959],{},[1900,1937,1938,1939,18],{},"Set it to ",[172,1940,1941],{},"REPLACE",[1882,1943,1944,1947,1950],{},[1900,1945,1946],{},"Checksum \"mismatch\" on large files",[1900,1948,1949],{},"Multipart ETag is not an MD5",[1900,1951,1952,1953,18],{},"Compare size, or use ",[172,1954,1955],{},"ChecksumAlgorithm",[1882,1957,1958,1964,1969],{},[1900,1959,1960,1963],{},[172,1961,1962],{},"AccessDenied"," on cleanup",[1900,1965,1966,1967],{},"Policy grants no ",[172,1968,245],{},[1900,1970,1971],{},"Add it for the report prefix.",[1882,1973,1974,1977,1982],{},[1900,1975,1976],{},"Presigned link expires early",[1900,1978,1979,1980],{},"Role session shorter than ",[172,1981,1806],{},[1900,1983,1984],{},"Sign with a longer-lived identity.",[1882,1986,1987,1990,1993],{},[1900,1988,1989],{},"Readers get a truncated file",[1900,1991,1992],{},"Uploaded straight to the final key",[1900,1994,1995],{},"Stage and promote.",[1882,1997,1998,2004,2007],{},[1900,1999,2000,2003],{},[172,2001,2002],{},"NoCredentialsError"," under cron",[1900,2005,2006],{},"Environment differs from the shell",[1900,2008,2009],{},"Use an instance role, or export credentials in the job.",[160,2011,2013],{"id":2012},"performance-and-scale-notes","Performance and scale notes",[20,2015,30,2022,30,2025,30,2028,30,2032,30,2037,30,2043,30,2047,30,2051,30,2056,30,2061,30,2090,30,2095,30,2097,30,2101,30,2121,30,2125,30,2130],{"viewBox":2016,"role":23,"ariaLabel":2017,"ariaLabelledBy":2018,"xmlns":28,"style":2021},"0 0 800 224","Where parallelism pays: splitting one upload into concurrent parts helps only on a fast link, while running several report builds concurrently overlaps the slow generation step as well.",[2019,2020],"par-t","par-d","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[32,2023,2024],{"id":2019},"Parallelise across reports, not within a single transfer",[36,2026,2027],{"id":2020},"Two timelines for a job producing four regional reports. In the first, reports are built and uploaded one after another, and tuning multipart concurrency only shortens the small upload segments. In the second, four workers build and upload concurrently, overlapping the much longer build segments as well, so total wall-clock time falls by roughly a factor of four rather than a few per cent.",[40,2029],{"x":595,"y":595,"width":2030,"height":2031,"fill":46},"800","224",[40,2033],{"x":2034,"y":2035,"width":49,"height":49,"rx":2036,"fill":84},"238","18","3",[58,2038,2042],{"x":2039,"y":2040,"style":2041},"260","29","font-size:11px;fill:var(--muted,#5b6780)","build the workbook",[40,2044],{"x":2045,"y":2035,"width":49,"height":49,"rx":2036,"fill":2046},"440","var(--teal,#14b8a6)",[58,2048,2050],{"x":2049,"y":2040,"style":2041},"462","upload",[40,2052],{"x":49,"y":2053,"width":1596,"height":2054,"rx":2055,"fill":144,"stroke":145},"54","26","6",[58,2057,2060],{"x":2058,"y":52,"style":2059},"62","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","sequential",[2062,2063,2064,2065,2064,2069,2064,2072,2064,2075,2064,2078,2064,2081,2064,2084,2064,2087,30],"g",{},"\n    ",[40,2066],{"x":2067,"y":2053,"width":66,"height":2054,"rx":2068,"fill":84},"120","4",[40,2070],{"x":2071,"y":2053,"width":2054,"height":2054,"rx":2068,"fill":2046},"232",[40,2073],{"x":2074,"y":2053,"width":66,"height":2054,"rx":2068,"fill":84},"258",[40,2076],{"x":2077,"y":2053,"width":2054,"height":2054,"rx":2068,"fill":2046},"370",[40,2079],{"x":2080,"y":2053,"width":66,"height":2054,"rx":2068,"fill":84},"396",[40,2082],{"x":2083,"y":2053,"width":2054,"height":2054,"rx":2068,"fill":2046},"508",[40,2085],{"x":2086,"y":2053,"width":66,"height":2054,"rx":2068,"fill":84},"534",[40,2088],{"x":2089,"y":2053,"width":2054,"height":2054,"rx":2068,"fill":2046},"646",[58,2091,2094],{"x":2092,"y":52,"style":2093},"690","font-size:10.5px;fill:var(--muted,#5b6780)","done",[40,2096],{"x":49,"y":96,"width":1596,"height":2054,"rx":2055,"fill":144,"stroke":145},[58,2098,2100],{"x":2058,"y":2099,"style":2059},"122","4 workers",[2062,2102,2064,2103,2064,2105,2064,2107,2064,2109,2064,2111,2064,2114,2064,2116,2064,2119,30],{},[40,2104],{"x":2067,"y":1596,"width":66,"height":2035,"rx":2068,"fill":84},[40,2106],{"x":2071,"y":1596,"width":2054,"height":2035,"rx":2068,"fill":2046},[40,2108],{"x":2067,"y":78,"width":66,"height":2035,"rx":2068,"fill":84},[40,2110],{"x":2071,"y":78,"width":2054,"height":2035,"rx":2068,"fill":2046},[40,2112],{"x":2067,"y":2113,"width":66,"height":2035,"rx":2068,"fill":84},"140",[40,2115],{"x":2071,"y":2113,"width":2054,"height":2035,"rx":2068,"fill":2046},[40,2117],{"x":2067,"y":2118,"width":66,"height":2035,"rx":2068,"fill":84},"162",[40,2120],{"x":2071,"y":2118,"width":2054,"height":2035,"rx":2068,"fill":2046},[75,2122],{"x1":2123,"y1":60,"x2":2123,"y2":89,"stroke":55,"style":2124},"266","stroke-width:2px;stroke-dasharray:4 4",[58,2126,2129],{"x":2127,"y":2113,"style":2128},"284","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca)","done here instead",[58,2131,2133],{"x":110,"y":2132,"style":157},"208","tuning multipart concurrency only shrinks the teal segments — the build dominates",[10,2135,2136,2137,1043],{},"boto3 switches to multipart uploads automatically above a threshold and runs the parts concurrently, so large reports already parallelise without any work from you. The knobs live on ",[172,2138,2139],{},"TransferConfig",[165,2141,2143],{"className":356,"code":2142,"language":358,"meta":170,"style":170},"import boto3\nfrom boto3.s3.transfer import TransferConfig\n\nconfig = TransferConfig(\n    multipart_threshold=16 * 1024 * 1024,   # start multipart at 16 MB\n    multipart_chunksize=16 * 1024 * 1024,\n    max_concurrency=8,\n    use_threads=True,\n)\n\ns3.upload_fileobj(buffer, bucket, key,\n                  ExtraArgs={\"ContentType\": XLSX}, Config=config)\n",[172,2144,2145,2151,2163,2167,2177,2203,2222,2234,2246,2250,2254,2259],{"__ignoreMap":170},[175,2146,2147,2149],{"class":75,"line":177},[175,2148,365],{"class":207},[175,2150,382],{"class":211},[175,2152,2153,2155,2158,2160],{"class":75,"line":221},[175,2154,977],{"class":207},[175,2156,2157],{"class":211}," boto3.s3.transfer ",[175,2159,365],{"class":207},[175,2161,2162],{"class":211}," TransferConfig\n",[175,2164,2165],{"class":75,"line":276},[175,2166,401],{"emptyLinePlaceholder":400},[175,2168,2169,2172,2174],{"class":75,"line":285},[175,2170,2171],{"class":211},"config ",[175,2173,215],{"class":207},[175,2175,2176],{"class":211}," TransferConfig(\n",[175,2178,2179,2182,2184,2187,2190,2193,2195,2197,2200],{"class":75,"line":298},[175,2180,2181],{"class":546},"    multipart_threshold",[175,2183,215],{"class":207},[175,2185,2186],{"class":406},"16",[175,2188,2189],{"class":207}," *",[175,2191,2192],{"class":406}," 1024",[175,2194,2189],{"class":207},[175,2196,2192],{"class":406},[175,2198,2199],{"class":211},",   ",[175,2201,2202],{"class":1126},"# start multipart at 16 MB\n",[175,2204,2205,2208,2210,2212,2214,2216,2218,2220],{"class":75,"line":323},[175,2206,2207],{"class":546},"    multipart_chunksize",[175,2209,215],{"class":207},[175,2211,2186],{"class":406},[175,2213,2189],{"class":207},[175,2215,2192],{"class":406},[175,2217,2189],{"class":207},[175,2219,2192],{"class":406},[175,2221,273],{"class":211},[175,2223,2224,2227,2229,2232],{"class":75,"line":334},[175,2225,2226],{"class":546},"    max_concurrency",[175,2228,215],{"class":207},[175,2230,2231],{"class":406},"8",[175,2233,273],{"class":211},[175,2235,2236,2239,2241,2244],{"class":75,"line":340},[175,2237,2238],{"class":546},"    use_threads",[175,2240,215],{"class":207},[175,2242,2243],{"class":406},"True",[175,2245,273],{"class":211},[175,2247,2248],{"class":75,"line":436},[175,2249,433],{"class":211},[175,2251,2252],{"class":75,"line":453},[175,2253,401],{"emptyLinePlaceholder":400},[175,2255,2256],{"class":75,"line":458},[175,2257,2258],{"class":211},"s3.upload_fileobj(buffer, bucket, key,\n",[175,2260,2261,2264,2266,2268,2270,2272,2274,2277,2280,2282],{"class":75,"line":469},[175,2262,2263],{"class":546},"                  ExtraArgs",[175,2265,215],{"class":207},[175,2267,630],{"class":211},[175,2269,633],{"class":184},[175,2271,267],{"class":211},[175,2273,407],{"class":406},[175,2275,2276],{"class":211},"}, ",[175,2278,2279],{"class":546},"Config",[175,2281,215],{"class":207},[175,2283,2284],{"class":211},"config)\n",[10,2286,2287,2288,2291],{},"Raising ",[172,2289,2290],{},"max_concurrency"," helps on a fast link and hurts on a constrained one, where parts start competing for the same bandwidth. Measure before tuning.",[10,2293,2294,2295,2299,2300,2303],{},"Three habits matter more than the knobs. ",[2296,2297,2298],"strong",{},"Reuse the client"," — ",[172,2301,2302],{},"boto3.client(\"s3\")"," builds a session and resolves credentials, so creating one per upload inside a loop is pure overhead:",[165,2305,2307],{"className":356,"code":2306,"language":358,"meta":170,"style":170},"s3 = boto3.client(\"s3\")            # once, at module level\n\nfor region in regions:\n    publish(build_report(region), bucket, f\"reports\u002F{region}-latest.xlsx\")\n",[172,2308,2309,2325,2329,2343],{"__ignoreMap":170},[175,2310,2311,2313,2315,2317,2319,2322],{"class":75,"line":177},[175,2312,422],{"class":211},[175,2314,215],{"class":207},[175,2316,427],{"class":211},[175,2318,430],{"class":184},[175,2320,2321],{"class":211},")            ",[175,2323,2324],{"class":1126},"# once, at module level\n",[175,2326,2327],{"class":75,"line":221},[175,2328,401],{"emptyLinePlaceholder":400},[175,2330,2331,2334,2337,2340],{"class":75,"line":276},[175,2332,2333],{"class":207},"for",[175,2335,2336],{"class":211}," region ",[175,2338,2339],{"class":207},"in",[175,2341,2342],{"class":211}," regions:\n",[175,2344,2345,2348,2351,2353,2355,2358,2360,2363],{"class":75,"line":285},[175,2346,2347],{"class":211},"    publish(build_report(region), bucket, ",[175,2349,2350],{"class":207},"f",[175,2352,1035],{"class":184},[175,2354,630],{"class":761},[175,2356,2357],{"class":211},"region",[175,2359,767],{"class":761},[175,2361,2362],{"class":184},"-latest.xlsx\"",[175,2364,433],{"class":211},[10,2366,2367,2370,2371,2373],{},[2296,2368,2369],{},"Copy server-side rather than re-uploading."," Promoting a dated object to the latest key with ",[172,2372,125],{}," moves no bytes over your connection at all.",[10,2375,2376,2379,2380,2384],{},[2296,2377,2378],{},"Parallelise across reports, not within one."," A fan-out job producing one workbook per region — the shape described in ",[14,2381,2383],{"href":2382},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","generating one Excel report per region"," — gains far more from a thread pool over regions than from tuning a single transfer:",[165,2386,2388],{"className":356,"code":2387,"language":358,"meta":170,"style":170},"from concurrent.futures import ThreadPoolExecutor\n\ndef build_and_publish(region):\n    buffer = build_report(region)                # returns a BytesIO\n    return publish(buffer, bucket, f\"reports\u002F{region}-latest.xlsx\")\n\nwith ThreadPoolExecutor(max_workers=6) as pool:\n    for uri in pool.map(build_and_publish, [\"north\", \"south\", \"west\", \"east\"]):\n        print(\"published\", uri)\n",[172,2389,2390,2402,2406,2416,2429,2450,2454,2475,2509],{"__ignoreMap":170},[175,2391,2392,2394,2397,2399],{"class":75,"line":177},[175,2393,977],{"class":207},[175,2395,2396],{"class":211}," concurrent.futures ",[175,2398,365],{"class":207},[175,2400,2401],{"class":211}," ThreadPoolExecutor\n",[175,2403,2404],{"class":75,"line":221},[175,2405,401],{"emptyLinePlaceholder":400},[175,2407,2408,2410,2413],{"class":75,"line":276},[175,2409,733],{"class":207},[175,2411,2412],{"class":736}," build_and_publish",[175,2414,2415],{"class":211},"(region):\n",[175,2417,2418,2421,2423,2426],{"class":75,"line":285},[175,2419,2420],{"class":211},"    buffer ",[175,2422,215],{"class":207},[175,2424,2425],{"class":211}," build_report(region)                ",[175,2427,2428],{"class":1126},"# returns a BytesIO\n",[175,2430,2431,2433,2436,2438,2440,2442,2444,2446,2448],{"class":75,"line":298},[175,2432,922],{"class":207},[175,2434,2435],{"class":211}," publish(buffer, bucket, ",[175,2437,2350],{"class":207},[175,2439,1035],{"class":184},[175,2441,630],{"class":761},[175,2443,2357],{"class":211},[175,2445,767],{"class":761},[175,2447,2362],{"class":184},[175,2449,433],{"class":211},[175,2451,2452],{"class":75,"line":323},[175,2453,401],{"emptyLinePlaceholder":400},[175,2455,2456,2458,2461,2464,2466,2468,2470,2472],{"class":75,"line":334},[175,2457,540],{"class":207},[175,2459,2460],{"class":211}," ThreadPoolExecutor(",[175,2462,2463],{"class":546},"max_workers",[175,2465,215],{"class":207},[175,2467,2055],{"class":406},[175,2469,555],{"class":211},[175,2471,392],{"class":207},[175,2473,2474],{"class":211}," pool:\n",[175,2476,2477,2480,2483,2485,2488,2491,2493,2496,2498,2501,2503,2506],{"class":75,"line":340},[175,2478,2479],{"class":207},"    for",[175,2481,2482],{"class":211}," uri ",[175,2484,2339],{"class":207},[175,2486,2487],{"class":211}," pool.map(build_and_publish, [",[175,2489,2490],{"class":184},"\"north\"",[175,2492,238],{"class":211},[175,2494,2495],{"class":184},"\"south\"",[175,2497,238],{"class":211},[175,2499,2500],{"class":184},"\"west\"",[175,2502,238],{"class":211},[175,2504,2505],{"class":184},"\"east\"",[175,2507,2508],{"class":211},"]):\n",[175,2510,2511,2514,2516,2519],{"class":75,"line":436},[175,2512,2513],{"class":406},"        print",[175,2515,654],{"class":211},[175,2517,2518],{"class":184},"\"published\"",[175,2520,2521],{"class":211},", uri)\n",[10,2523,2524,2525,2529],{},"Threads are the right choice here rather than processes, because the work is network-bound and boto3 releases the GIL during I\u002FO. Watch memory, though: six workers each holding a workbook buffer means six copies resident at once, which for large reports is the real constraint — the streaming techniques in ",[14,2526,2528],{"href":2527},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fwrite-large-dataframes-to-excel-with-write-only-mode\u002F","writing large DataFrames with write-only mode"," keep each one smaller.",[160,2531,2533],{"id":2532},"conclusion","Conclusion",[10,2535,2536,2537,2539,2540,2542],{},"Publishing an Excel report to S3 is ",[172,2538,98],{}," plus four disciplines. Build the workbook in a ",[172,2541,68],{}," and rewind it. Set the spreadsheetml content type so browsers treat the object as a spreadsheet. Stage the upload under a temporary key and promote it with a server-side copy, so readers never see a partial file. Verify size — and a checksum when the object is not multipart. Then share with a short-lived presigned URL rather than making the bucket public, and let a dated key plus a stable latest key give you history and durable links at the same time.",[160,2544,2546],{"id":2545},"frequently-asked-questions","Frequently asked questions",[10,2548,2549,2552,2553,2556,2557,2559],{},[2296,2550,2551],{},"Do I have to write the file to disk before uploading?","\nNo. Write the workbook into an ",[172,2554,2555],{},"io.BytesIO"," buffer and pass it to ",[172,2558,98],{},". That avoids temporary files entirely, which matters in a container with a read-only or ephemeral filesystem.",[10,2561,2562,2569,2572,2573,2575],{},[2296,2563,2564,2565,2568],{},"Which content type should an ",[172,2566,2567],{},".xlsx"," have?",[172,2570,2571],{},"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet",". Without it S3 serves the object as ",[172,2574,675],{}," and browsers download it with a generic name instead of opening it as a spreadsheet.",[10,2577,2578,2581],{},[2296,2579,2580],{},"Why does the ETag not match my file's MD5?","\nBecause the upload was multipart. For multipart objects the ETag is a digest of the part digests with a dash and the part count appended, so compare sizes instead, or enable S3's own checksum algorithms.",[10,2583,2584,2587],{},[2296,2585,2586],{},"How do I share the report without making the bucket public?","\nGenerate a presigned URL. It grants time-limited access to one object using your credentials, so the bucket stays private and the link expires on its own.",[10,2589,2590,2593],{},[2296,2591,2592],{},"Should each run overwrite the same key?","\nWrite a dated key for the history and copy it over a stable latest key for links. Enabling bucket versioning gives you a safety net on top, so an accidental overwrite is recoverable.",[160,2595,2597],{"id":2596},"related","Related",[2599,2600,2601,2608,2615,2622,2629],"ul",{},[2602,2603,2604,2605,2607],"li",{},"Up to the parent: ",[14,2606,17],{"href":16}," — the naming, atomicity and retry patterns applied here.",[2602,2609,2610,2614],{},[14,2611,2613],{"href":2612},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python\u002F","Upload an Excel Report to SharePoint with Python"," — the same job against Microsoft Graph.",[2602,2616,2617,2621],{},[14,2618,2620],{"href":2619},"\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 on-premise equivalent.",[2602,2623,2624,2628],{},[14,2625,2627],{"href":2626},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","Retry a Failed Excel Report Job in Python"," — backoff for the transient upload failures.",[2602,2630,2631,2634],{},[14,2632,2633],{"href":2382},"Generate One Excel Report per Region in a Loop"," — the fan-out this upload step serves.",[2636,2637,2638],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sL7So, html code.shiki .sL7So{--shiki-default:#005CC5;--shiki-dark:#72F088}",{"title":170,"searchDepth":221,"depth":221,"links":2640},[2641,2642,2643,2644,2645,2646,2647,2648,2649,2650],{"id":162,"depth":221,"text":163},{"id":346,"depth":221,"text":347},{"id":679,"depth":221,"text":680},{"id":1204,"depth":221,"text":1205},{"id":1568,"depth":221,"text":1569},{"id":1873,"depth":221,"text":1874},{"id":2012,"depth":221,"text":2013},{"id":2532,"depth":221,"text":2533},{"id":2545,"depth":221,"text":2546},{"id":2596,"depth":221,"text":2597},"2026-08-15","Publish a generated .xlsx to S3 from Python: upload straight from memory, set the right content type, make the write atomic, verify it, and share it with a presigned URL.","md",[2655,2657,2660,2662,2664],{"q":2551,"a":2656},"No. Write the workbook into an io.BytesIO buffer and pass it to upload_fileobj. That avoids temporary files entirely, which matters in a container with a read-only or ephemeral filesystem.",{"q":2658,"a":2659},"Which content type should an .xlsx have?","application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet. Without it S3 serves the object as binary\u002Foctet-stream and browsers download it with a generic name instead of opening it as a spreadsheet.",{"q":2580,"a":2661},"Because the upload was multipart. For multipart objects the ETag is a digest of the part digests with a dash and the part count appended, so compare sizes instead, or enable S3's own checksum algorithms.",{"q":2586,"a":2663},"Generate a presigned URL. It grants time-limited access to one object using your credentials, so the bucket stays private and the link expires on its own.",{"q":2592,"a":2665},"Write a dated key for the history and copy it over a stable latest key for links. Enabling bucket versioning gives you a safety net on top, so an accidental overwrite is recoverable.",{},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3",{"title":2669,"description":2670},"Upload an Excel File to S3 with boto3 (Python)","Step-by-step boto3 upload for Excel reports — in-memory uploads, the xlsx content type, staging keys for atomicity, integrity checks, presigned links and retries.","upload-an-excel-report-to-amazon-s3-with-boto3","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002Findex","how-to","vBdAjLlsJpqNUHbB7P7dod1bKPCOjhGVWztFJuqvWNY",[2676,2680],{"title":2677,"path":2678,"stem":2679,"children":-1},"Save Excel Reports to Google Drive with Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fsave-excel-reports-to-google-drive-with-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fsave-excel-reports-to-google-drive-with-python\u002Findex",{"title":2613,"path":2681,"stem":2682,"children":-1},"\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",1786800027148]