[{"data":1,"prerenderedAt":1901},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage":1868,"ancestors:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage":1876,"children:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage":1881,"related:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage":1882},{"id":4,"title":5,"body":6,"dateModified":1836,"datePublished":1836,"description":1837,"extension":1838,"faq":1839,"keyword":1848,"kind":1849,"meta":1850,"navigation":285,"path":1858,"seo":1859,"slug":1862,"stem":1863,"topics":1864,"type":1866,"__hash__":1867},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage\u002Findex.md","Upload an Excel Report to Azure Blob Storage with Python",{"type":7,"value":8,"toc":1823},"minimark",[9,19,202,207,241,252,255,405,409,425,496,499,545,549,556,942,967,970,1098,1102,1108,1160,1163,1200,1204,1207,1395,1417,1421,1424,1480,1483,1487,1494,1550,1561,1687,1691,1745,1749,1758,1762,1768,1774,1780,1786,1790,1819],[10,11,12,13,18],"p",{},"Azure Blob Storage is a natural place to publish reports when the rest of the organisation lives in Azure: cheap, durable, reachable from Power BI, Data Factory and a browser link. The SDK makes the upload a single call, but a report store needs more than an upload — predictable names, a download that opens in Excel, protection against overwriting last month's published file, and a way to prove what was uploaded. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",", covers each of those. Every call was run against Azurite, Microsoft's local storage emulator.",[20,21,29,30,29,34,29,38,29,46,29,56,29,67,29,74,29,79,29,85,29,90,29,93,29,99,29,104,29,108,29,112,29,116,29,119,29,124,29,131,29,137,29,141,29,144,29,148,29,152,29,155,29,158,29,164,29,168,29,172,29,175,29,179,29,182,29,187,29,194,29,199],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"4 0 752 160","img",[25,26],"az-flow-t","az-flow-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  ",[31,32,33],"title",{"id":25},"Publishing one report to Blob Storage",[35,36,37],"desc",{"id":26},"The script signs in with a managed identity or developer login, builds a blob name from the report period, uploads the bytes with the spreadsheet content type and a SHA-256 checksum in metadata while refusing to overwrite, checks the stored MD5 against the local file, and finally creates a read-only link that expires in seven days.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"4","0","752","160","#ffffff",[39,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"20.0","16.0","115.2","128.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,60],"g",{"transform":59},"translate(65.6,46.5) scale(1.000)",[61,62],"path",{"d":63,"fill":64,"stroke":65,"style":66},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[68,69,73],"text",{"x":70,"y":71,"style":72},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Sign in",[68,75,78],{"x":70,"y":76,"style":77},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","no keys in code",[80,81],"line",{"x1":82,"y1":83,"x2":84,"y2":83,"stroke":54,"style":55},"140.2","80.0","162.2",[86,87],"polygon",{"points":88,"fill":89},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[39,91],{"x":92,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"171.2",[57,94,96],{"transform":95},"translate(216.8,37.8) scale(1.000)",[61,97],{"d":98,"fill":64,"stroke":65,"style":66},"M3 6h6l2 2h10v11H3z",[68,100,103],{"x":101,"y":102,"style":72},"228.8","84.2","Name by",[68,105,107],{"x":101,"y":106,"style":72},"101.8","period",[68,109,111],{"x":101,"y":110,"style":77},"121.2","monthly\u002F2026\u002F09\u002F…",[80,113],{"x1":114,"y1":83,"x2":115,"y2":83,"stroke":54,"style":55},"291.4","313.4",[86,117],{"points":118,"fill":89},"317.4,80.0 308.4,85.0 308.4,75.0",[39,120],{"x":121,"y":49,"width":50,"height":51,"rx":52,"fill":122,"stroke":123,"style":55},"322.4","#fdefd8","var(--gold,#b4740a)",[57,125,127],{"transform":126},"translate(368.0,29.0) scale(1.000)",[61,128],{"d":129,"fill":64,"stroke":130,"style":66},"M3 11l18-8-8 18-2-8z","var(--gold-ink,#7a4e06)",[68,132,136],{"x":133,"y":134,"style":135},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Upload",[68,138,140],{"x":133,"y":139,"style":77},"95.0","type,",[68,142,143],{"x":133,"y":76,"style":77},"checksum,",[68,145,147],{"x":133,"y":146,"style":77},"130.0","overwrite=False",[80,149],{"x1":150,"y1":83,"x2":151,"y2":83,"stroke":54,"style":55},"442.6","464.6",[86,153],{"points":154,"fill":89},"468.6,80.0 459.6,85.0 459.6,75.0",[39,156],{"x":157,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"473.6",[57,159,161],{"transform":160},"translate(519.2,37.8) scale(1.000)",[61,162],{"d":163,"fill":64,"stroke":65,"style":66},"M5 12.5l4.5 4.5L19 7",[68,165,167],{"x":166,"y":102,"style":72},"531.2","Verify",[68,169,171],{"x":166,"y":170,"style":77},"103.8","stored MD5",[68,173,174],{"x":166,"y":110,"style":77},"matches",[80,176],{"x1":177,"y1":83,"x2":178,"y2":83,"stroke":54,"style":55},"593.8","615.8",[86,180],{"points":181,"fill":89},"619.8,80.0 610.8,85.0 610.8,75.0",[39,183],{"x":184,"y":49,"width":50,"height":51,"rx":52,"fill":185,"stroke":186,"style":55},"624.8","#d9f4f1","var(--teal,#0f9488)",[57,188,190],{"transform":189},"translate(670.4,46.5) scale(1.000)",[61,191],{"d":192,"fill":64,"stroke":193,"style":66},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1","var(--teal-ink,#0b6157)",[68,195,198],{"x":196,"y":71,"style":197},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Share",[68,200,201],{"x":196,"y":76,"style":77},"read-only link",[203,204,206],"h2",{"id":205},"prerequisites","Prerequisites",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install azure-storage-blob azure-identity pandas openpyxl\n","bash","",[215,216,217],"code",{"__ignoreMap":213},[218,219,221,225,229,232,235,238],"span",{"class":80,"line":220},1,[218,222,224],{"class":223},"sMTad","pip",[218,226,228],{"class":227},"srMev"," install",[218,230,231],{"class":227}," azure-storage-blob",[218,233,234],{"class":227}," azure-identity",[218,236,237],{"class":227}," pandas",[218,239,240],{"class":227}," openpyxl\n",[10,242,243,244,247,248,251],{},"You need a storage account and a container — ",[215,245,246],{},"reports"," below. For local development, Azurite runs the same API on your machine (",[215,249,250],{},"npx azurite-blob","), and its well-known development connection string works with every call in this guide.",[10,253,254],{},"A report to publish, built in memory:",[208,256,260],{"className":257,"code":258,"language":259,"meta":213,"style":213},"language-python shiki shiki-themes github-light github-dark-high-contrast","import hashlib\nimport io\n\nimport pandas as pd\n\nbuffer = io.BytesIO()\npd.DataFrame({\"region\": [\"North\", \"South\", \"East\", \"West\"],\n              \"sales\": [182_400, 141_950, 97_300, 120_880]}).to_excel(buffer, index=False)\nreport_bytes = buffer.getvalue()\n","python",[215,261,262,272,280,287,301,306,318,352,394],{"__ignoreMap":213},[218,263,264,268],{"class":80,"line":220},[218,265,267],{"class":266},"s-kum","import",[218,269,271],{"class":270},"skGVy"," hashlib\n",[218,273,275,277],{"class":80,"line":274},2,[218,276,267],{"class":266},[218,278,279],{"class":270}," io\n",[218,281,283],{"class":80,"line":282},3,[218,284,286],{"emptyLinePlaceholder":285},true,"\n",[218,288,290,292,295,298],{"class":80,"line":289},4,[218,291,267],{"class":266},[218,293,294],{"class":270}," pandas ",[218,296,297],{"class":266},"as",[218,299,300],{"class":270}," pd\n",[218,302,304],{"class":80,"line":303},5,[218,305,286],{"emptyLinePlaceholder":285},[218,307,309,312,315],{"class":80,"line":308},6,[218,310,311],{"class":270},"buffer ",[218,313,314],{"class":266},"=",[218,316,317],{"class":270}," io.BytesIO()\n",[218,319,321,324,327,330,333,336,339,341,344,346,349],{"class":80,"line":320},7,[218,322,323],{"class":270},"pd.DataFrame({",[218,325,326],{"class":227},"\"region\"",[218,328,329],{"class":270},": [",[218,331,332],{"class":227},"\"North\"",[218,334,335],{"class":270},", ",[218,337,338],{"class":227},"\"South\"",[218,340,335],{"class":270},[218,342,343],{"class":227},"\"East\"",[218,345,335],{"class":270},[218,347,348],{"class":227},"\"West\"",[218,350,351],{"class":270},"],\n",[218,353,355,358,360,364,366,369,371,374,376,379,382,386,388,391],{"class":80,"line":354},8,[218,356,357],{"class":227},"              \"sales\"",[218,359,329],{"class":270},[218,361,363],{"class":362},"sP0c6","182_400",[218,365,335],{"class":270},[218,367,368],{"class":362},"141_950",[218,370,335],{"class":270},[218,372,373],{"class":362},"97_300",[218,375,335],{"class":270},[218,377,378],{"class":362},"120_880",[218,380,381],{"class":270},"]}).to_excel(buffer, ",[218,383,385],{"class":384},"sa561","index",[218,387,314],{"class":266},[218,389,390],{"class":362},"False",[218,392,393],{"class":270},")\n",[218,395,397,400,402],{"class":80,"line":396},9,[218,398,399],{"class":270},"report_bytes ",[218,401,314],{"class":266},[218,403,404],{"class":270}," buffer.getvalue()\n",[203,406,408],{"id":407},"step-1-sign-in-without-putting-a-key-in-the-code","Step 1 — Sign in without putting a key in the code",[10,410,411,412,415,416,419,420,424],{},"In Azure, use ",[215,413,414],{},"DefaultAzureCredential",": it picks up a managed identity on a VM, container or Function, and your ",[215,417,418],{},"az login"," session on a developer machine, so the same code runs in both places without a secret. Grant the identity ",[421,422,423],"strong",{},"Storage Blob Data Contributor"," on the container — the Owner or Contributor role on the account manages it but does not grant access to blob data through Azure AD:",[208,426,428],{"className":257,"code":427,"language":259,"meta":213,"style":213},"from azure.identity import DefaultAzureCredential\nfrom azure.storage.blob import BlobServiceClient\n\nservice = BlobServiceClient(\n    account_url=\"https:\u002F\u002Freportstore.blob.core.windows.net\",\n    credential=DefaultAzureCredential(),\n)\n",[215,429,430,443,455,459,469,482,492],{"__ignoreMap":213},[218,431,432,435,438,440],{"class":80,"line":220},[218,433,434],{"class":266},"from",[218,436,437],{"class":270}," azure.identity ",[218,439,267],{"class":266},[218,441,442],{"class":270}," DefaultAzureCredential\n",[218,444,445,447,450,452],{"class":80,"line":274},[218,446,434],{"class":266},[218,448,449],{"class":270}," azure.storage.blob ",[218,451,267],{"class":266},[218,453,454],{"class":270}," BlobServiceClient\n",[218,456,457],{"class":80,"line":282},[218,458,286],{"emptyLinePlaceholder":285},[218,460,461,464,466],{"class":80,"line":289},[218,462,463],{"class":270},"service ",[218,465,314],{"class":266},[218,467,468],{"class":270}," BlobServiceClient(\n",[218,470,471,474,476,479],{"class":80,"line":303},[218,472,473],{"class":384},"    account_url",[218,475,314],{"class":266},[218,477,478],{"class":227},"\"https:\u002F\u002Freportstore.blob.core.windows.net\"",[218,480,481],{"class":270},",\n",[218,483,484,487,489],{"class":80,"line":308},[218,485,486],{"class":384},"    credential",[218,488,314],{"class":266},[218,490,491],{"class":270},"DefaultAzureCredential(),\n",[218,493,494],{"class":80,"line":320},[218,495,393],{"class":270},[10,497,498],{},"Outside Azure, or against Azurite, a connection string works — keep it in an environment variable, never in the script:",[208,500,502],{"className":257,"code":501,"language":259,"meta":213,"style":213},"import os\n\nservice = BlobServiceClient.from_connection_string(os.environ[\"AZURE_STORAGE_CONNECTION_STRING\"])\ncontainer = service.get_container_client(\"reports\")\n",[215,503,504,511,515,530],{"__ignoreMap":213},[218,505,506,508],{"class":80,"line":220},[218,507,267],{"class":266},[218,509,510],{"class":270}," os\n",[218,512,513],{"class":80,"line":274},[218,514,286],{"emptyLinePlaceholder":285},[218,516,517,519,521,524,527],{"class":80,"line":282},[218,518,463],{"class":270},[218,520,314],{"class":266},[218,522,523],{"class":270}," BlobServiceClient.from_connection_string(os.environ[",[218,525,526],{"class":227},"\"AZURE_STORAGE_CONNECTION_STRING\"",[218,528,529],{"class":270},"])\n",[218,531,532,535,537,540,543],{"class":80,"line":289},[218,533,534],{"class":270},"container ",[218,536,314],{"class":266},[218,538,539],{"class":270}," service.get_container_client(",[218,541,542],{"class":227},"\"reports\"",[218,544,393],{"class":270},[203,546,548],{"id":547},"step-2-upload-with-a-name-type-and-checksum","Step 2 — Upload with a name, type and checksum",[10,550,551,552,555],{},"Blob names can contain slashes, which tools show as folders. Put the period in the name so reports sort naturally and never collide. Set the content type so a download opens in Excel, a ",[215,553,554],{},"Content-Disposition"," so browsers save it under a sensible file name, and record a SHA-256 of the bytes in metadata:",[208,557,559],{"className":257,"code":558,"language":259,"meta":213,"style":213},"from azure.core.exceptions import ResourceExistsError\nfrom azure.storage.blob import ContentSettings\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ndef publish(container, data, period, name=\"sales\"):\n    filename = f\"{name}_{period}.xlsx\"\n    blob = container.get_blob_client(f\"monthly\u002F{period[:4]}\u002F{period[5:]}\u002F{filename}\")\n    result = blob.upload_blob(\n        data,\n        overwrite=False,\n        content_settings=ContentSettings(\n            content_type=XLSX,\n            content_disposition=f'attachment; filename=\"{filename}\"',\n        ),\n        metadata={\"period\": period, \"sha256\": hashlib.sha256(data).hexdigest()},\n    )\n    if result[\"content_md5\"] is not None and bytes(result[\"content_md5\"]) != hashlib.md5(data).digest():\n        raise IOError(f\"{blob.blob_name}: stored MD5 does not match the local file\")\n    return blob\n\nblob = publish(container, report_bytes, \"2026-09\")\nprint(blob.blob_name)\n# monthly\u002F2026\u002F09\u002Fsales_2026-09.xlsx\n",[215,560,561,573,584,588,599,603,623,658,715,725,731,743,754,766,790,796,818,824,868,896,905,910,926,935],{"__ignoreMap":213},[218,562,563,565,568,570],{"class":80,"line":220},[218,564,434],{"class":266},[218,566,567],{"class":270}," azure.core.exceptions ",[218,569,267],{"class":266},[218,571,572],{"class":270}," ResourceExistsError\n",[218,574,575,577,579,581],{"class":80,"line":274},[218,576,434],{"class":266},[218,578,449],{"class":270},[218,580,267],{"class":266},[218,582,583],{"class":270}," ContentSettings\n",[218,585,586],{"class":80,"line":282},[218,587,286],{"emptyLinePlaceholder":285},[218,589,590,593,596],{"class":80,"line":289},[218,591,592],{"class":362},"XLSX",[218,594,595],{"class":266}," =",[218,597,598],{"class":227}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[218,600,601],{"class":80,"line":303},[218,602,286],{"emptyLinePlaceholder":285},[218,604,605,608,612,615,617,620],{"class":80,"line":308},[218,606,607],{"class":266},"def",[218,609,611],{"class":610},"s_Opv"," publish",[218,613,614],{"class":270},"(container, data, period, name",[218,616,314],{"class":266},[218,618,619],{"class":227},"\"sales\"",[218,621,622],{"class":270},"):\n",[218,624,625,628,630,633,636,640,643,646,649,651,653,655],{"class":80,"line":320},[218,626,627],{"class":270},"    filename ",[218,629,314],{"class":266},[218,631,632],{"class":266}," f",[218,634,635],{"class":227},"\"",[218,637,639],{"class":638},"sSjpA","{",[218,641,642],{"class":270},"name",[218,644,645],{"class":638},"}",[218,647,648],{"class":227},"_",[218,650,639],{"class":638},[218,652,107],{"class":270},[218,654,645],{"class":638},[218,656,657],{"class":227},".xlsx\"\n",[218,659,660,663,665,668,671,674,676,679,681,684,686,689,691,694,697,700,702,704,706,709,711,713],{"class":80,"line":354},[218,661,662],{"class":270},"    blob ",[218,664,314],{"class":266},[218,666,667],{"class":270}," container.get_blob_client(",[218,669,670],{"class":266},"f",[218,672,673],{"class":227},"\"monthly\u002F",[218,675,639],{"class":638},[218,677,678],{"class":270},"period[:",[218,680,41],{"class":362},[218,682,683],{"class":270},"]",[218,685,645],{"class":638},[218,687,688],{"class":227},"\u002F",[218,690,639],{"class":638},[218,692,693],{"class":270},"period[",[218,695,696],{"class":362},"5",[218,698,699],{"class":270},":]",[218,701,645],{"class":638},[218,703,688],{"class":227},[218,705,639],{"class":638},[218,707,708],{"class":270},"filename",[218,710,645],{"class":638},[218,712,635],{"class":227},[218,714,393],{"class":270},[218,716,717,720,722],{"class":80,"line":396},[218,718,719],{"class":270},"    result ",[218,721,314],{"class":266},[218,723,724],{"class":270}," blob.upload_blob(\n",[218,726,728],{"class":80,"line":727},10,[218,729,730],{"class":270},"        data,\n",[218,732,734,737,739,741],{"class":80,"line":733},11,[218,735,736],{"class":384},"        overwrite",[218,738,314],{"class":266},[218,740,390],{"class":362},[218,742,481],{"class":270},[218,744,746,749,751],{"class":80,"line":745},12,[218,747,748],{"class":384},"        content_settings",[218,750,314],{"class":266},[218,752,753],{"class":270},"ContentSettings(\n",[218,755,757,760,762,764],{"class":80,"line":756},13,[218,758,759],{"class":384},"            content_type",[218,761,314],{"class":266},[218,763,592],{"class":362},[218,765,481],{"class":270},[218,767,769,772,774,776,779,781,783,785,788],{"class":80,"line":768},14,[218,770,771],{"class":384},"            content_disposition",[218,773,314],{"class":266},[218,775,670],{"class":266},[218,777,778],{"class":227},"'attachment; filename=\"",[218,780,639],{"class":638},[218,782,708],{"class":270},[218,784,645],{"class":638},[218,786,787],{"class":227},"\"'",[218,789,481],{"class":270},[218,791,793],{"class":80,"line":792},15,[218,794,795],{"class":270},"        ),\n",[218,797,799,802,804,806,809,812,815],{"class":80,"line":798},16,[218,800,801],{"class":384},"        metadata",[218,803,314],{"class":266},[218,805,639],{"class":270},[218,807,808],{"class":227},"\"period\"",[218,810,811],{"class":270},": period, ",[218,813,814],{"class":227},"\"sha256\"",[218,816,817],{"class":270},": hashlib.sha256(data).hexdigest()},\n",[218,819,821],{"class":80,"line":820},17,[218,822,823],{"class":270},"    )\n",[218,825,827,830,833,836,839,842,845,848,851,854,857,859,862,865],{"class":80,"line":826},18,[218,828,829],{"class":266},"    if",[218,831,832],{"class":270}," result[",[218,834,835],{"class":227},"\"content_md5\"",[218,837,838],{"class":270},"] ",[218,840,841],{"class":266},"is",[218,843,844],{"class":266}," not",[218,846,847],{"class":362}," None",[218,849,850],{"class":266}," and",[218,852,853],{"class":362}," bytes",[218,855,856],{"class":270},"(result[",[218,858,835],{"class":227},[218,860,861],{"class":270},"]) ",[218,863,864],{"class":266},"!=",[218,866,867],{"class":270}," hashlib.md5(data).digest():\n",[218,869,871,874,877,880,882,884,886,889,891,894],{"class":80,"line":870},19,[218,872,873],{"class":266},"        raise",[218,875,876],{"class":362}," IOError",[218,878,879],{"class":270},"(",[218,881,670],{"class":266},[218,883,635],{"class":227},[218,885,639],{"class":638},[218,887,888],{"class":270},"blob.blob_name",[218,890,645],{"class":638},[218,892,893],{"class":227},": stored MD5 does not match the local file\"",[218,895,393],{"class":270},[218,897,899,902],{"class":80,"line":898},20,[218,900,901],{"class":266},"    return",[218,903,904],{"class":270}," blob\n",[218,906,908],{"class":80,"line":907},21,[218,909,286],{"emptyLinePlaceholder":285},[218,911,913,916,918,921,924],{"class":80,"line":912},22,[218,914,915],{"class":270},"blob ",[218,917,314],{"class":266},[218,919,920],{"class":270}," publish(container, report_bytes, ",[218,922,923],{"class":227},"\"2026-09\"",[218,925,393],{"class":270},[218,927,929,932],{"class":80,"line":928},23,[218,930,931],{"class":362},"print",[218,933,934],{"class":270},"(blob.blob_name)\n",[218,936,938],{"class":80,"line":937},24,[218,939,941],{"class":940},"s-wDw","# monthly\u002F2026\u002F09\u002Fsales_2026-09.xlsx\n",[10,943,944,946,947,950,951,954,955,957,958,961,962,966],{},[215,945,147],{}," is the important argument. It is also the SDK's default, which the SDK enforces by sending ",[215,948,949],{},"If-None-Match: *"," — spell it out anyway, so nobody \"fixes\" a failed rerun by flipping it without thinking. With ",[215,952,953],{},"overwrite=True",", a rerun of last month's job or a mistyped period would silently replace a report people have already read. With ",[215,956,390],{},", the second upload fails with ",[215,959,960],{},"ResourceExistsError: The specified blob already exists."," — a failure you can alert on, as in ",[14,963,965],{"href":964},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fsend-an-alert-when-an-excel-report-job-fails\u002F","Send an Alert When an Excel Report Job Fails",".",[10,968,969],{},"The service returns an MD5 of what it stored for uploads sent in a single request, which the SDK does for files up to 64 MB by default; comparing it with the local hash proves the bytes arrived intact. Larger uploads are sent in blocks and do not get a whole-file MD5, which is why the SHA-256 in metadata is the checksum to rely on.",[20,971,29,976,29,979,29,982,29,985,29,989,29,995,29,1001,29,1006,29,1013,29,1018,29,1023,29,1025,29,1030,29,1033,29,1041,29,1046,29,1051,29,1054,29,1059,29,1064,29,1067,29,1070,29,1075,29,1080,29,1083,29,1086,29,1093],{"viewBox":972,"role":23,"ariaLabelledBy":973,"xmlns":27,"style":28},"4 0 752 322",[974,975],"az-seq-t","az-seq-d",[31,977,978],{"id":974},"One upload, and what comes back",[35,980,981],{"id":975},"The script sends Put Blob with the bytes, the content type, the content disposition and metadata, and the If-None-Match condition that overwrite=False adds. The service stores the blob and replies 201 Created with an ETag and the MD5 of the stored bytes, which the script compares with its own hash. A second attempt with the same name receives 409 BlobAlreadyExists.",[39,983],{"x":41,"y":42,"width":43,"height":984,"fill":45},"322",[39,986],{"x":48,"y":49,"width":987,"height":988,"rx":52,"fill":53,"stroke":54,"style":55},"180.0","78.0",[57,990,992],{"transform":991},"translate(98.0,30.5) scale(1.000)",[61,993],{"d":994,"fill":64,"stroke":65,"style":66},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01",[68,996,1000],{"x":997,"y":998,"style":999},"110.0","76.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Report script",[39,1002],{"x":1003,"y":49,"width":987,"height":988,"rx":52,"fill":1004,"stroke":1005,"style":55},"560.0","#e7ebef","var(--line,#cdd5e6)",[57,1007,1009],{"transform":1008},"translate(638.0,30.5) scale(1.000)",[61,1010],{"d":1011,"fill":64,"stroke":1012,"style":66},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3","var(--text,#172033)",[68,1014,1017],{"x":1015,"y":998,"style":1016},"650.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Blob Storage",[80,1019],{"x1":997,"y1":1020,"x2":997,"y2":1021,"stroke":1005,"style":1022},"94.0","306.0","stroke-width:1.5px;stroke-dasharray:5 4",[80,1024],{"x1":1015,"y1":1020,"x2":1015,"y2":1021,"stroke":1005,"style":1022},[80,1026],{"x1":1027,"y1":1028,"x2":1029,"y2":1028,"stroke":54,"style":55},"115.0","134.0","641.0",[86,1031],{"points":1032,"fill":89},"645.0,134.0 636.0,139.0 636.0,129.0",[39,1034],{"x":1035,"y":1036,"width":1037,"height":1038,"rx":1039,"fill":53,"stroke":54,"style":1040},"235.9","105.0","288.2","21.0","10","stroke-width:1.2px",[68,1042,1045],{"x":133,"y":1043,"style":1044},"119.5","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","1. PUT blob, type, metadata, If-None-Match: *",[80,1047],{"x1":1048,"y1":1049,"x2":1050,"y2":1049,"stroke":54,"style":55},"645.0","182.0","119.0",[86,1052],{"points":1053,"fill":89},"115.0,182.0 124.0,177.0 124.0,187.0",[39,1055],{"x":1056,"y":1057,"width":1058,"height":1038,"rx":1039,"fill":185,"stroke":186,"style":1040},"266.0","153.0","228.0",[68,1060,1063],{"x":133,"y":1061,"style":1062},"167.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","2. 201 Created, ETag, Content-MD5",[80,1065],{"x1":1027,"y1":1066,"x2":1029,"y2":1066,"stroke":54,"style":55},"230.0",[86,1068],{"points":1069,"fill":89},"645.0,230.0 636.0,235.0 636.0,225.0",[39,1071],{"x":1072,"y":1073,"width":1074,"height":1038,"rx":1039,"fill":45,"stroke":1005,"style":1040},"312.2","201.0","135.6",[68,1076,1079],{"x":133,"y":1077,"style":1078},"215.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","3. same name again",[80,1081],{"x1":1048,"y1":1082,"x2":1050,"y2":1082,"stroke":54,"style":55},"278.0",[86,1084],{"points":1085,"fill":89},"115.0,278.0 124.0,273.0 124.0,283.0",[39,1087],{"x":1088,"y":1089,"width":1090,"height":1038,"rx":1039,"fill":1091,"stroke":1092,"style":1040},"295.9","249.0","168.2","#fce9e9","var(--danger,#dc2626)",[68,1094,1097],{"x":133,"y":1095,"style":1096},"263.5","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","4. 409 BlobAlreadyExists",[203,1099,1101],{"id":1100},"step-3-keep-a-stable-latest-name-as-well","Step 3 — Keep a stable \"latest\" name as well",[10,1103,1104,1105,1107],{},"Dashboards and colleagues often want one address that always holds the current report. Upload a second copy under a fixed name with ",[215,1106,953],{}," — that name is meant to be replaced, while the dated copy remains the record:",[208,1109,1111],{"className":257,"code":1110,"language":259,"meta":213,"style":213},"latest = container.get_blob_client(\"latest\u002Fsales.xlsx\")\nlatest.upload_blob(report_bytes, overwrite=True, content_settings=ContentSettings(content_type=XLSX))\n",[215,1112,1113,1127],{"__ignoreMap":213},[218,1114,1115,1118,1120,1122,1125],{"class":80,"line":220},[218,1116,1117],{"class":270},"latest ",[218,1119,314],{"class":266},[218,1121,667],{"class":270},[218,1123,1124],{"class":227},"\"latest\u002Fsales.xlsx\"",[218,1126,393],{"class":270},[218,1128,1129,1132,1135,1137,1140,1142,1145,1147,1150,1153,1155,1157],{"class":80,"line":274},[218,1130,1131],{"class":270},"latest.upload_blob(report_bytes, ",[218,1133,1134],{"class":384},"overwrite",[218,1136,314],{"class":266},[218,1138,1139],{"class":362},"True",[218,1141,335],{"class":270},[218,1143,1144],{"class":384},"content_settings",[218,1146,314],{"class":266},[218,1148,1149],{"class":270},"ContentSettings(",[218,1151,1152],{"class":384},"content_type",[218,1154,314],{"class":266},[218,1156,592],{"class":362},[218,1158,1159],{"class":270},"))\n",[10,1161,1162],{},"List the archive by prefix to see what has been published:",[208,1164,1166],{"className":257,"code":1165,"language":259,"meta":213,"style":213},"for item in container.list_blobs(name_starts_with=\"monthly\u002F2026\u002F\"):\n    print(item.name, item.size)\n",[215,1167,1168,1192],{"__ignoreMap":213},[218,1169,1170,1173,1176,1179,1182,1185,1187,1190],{"class":80,"line":220},[218,1171,1172],{"class":266},"for",[218,1174,1175],{"class":270}," item ",[218,1177,1178],{"class":266},"in",[218,1180,1181],{"class":270}," container.list_blobs(",[218,1183,1184],{"class":384},"name_starts_with",[218,1186,314],{"class":266},[218,1188,1189],{"class":227},"\"monthly\u002F2026\u002F\"",[218,1191,622],{"class":270},[218,1193,1194,1197],{"class":80,"line":274},[218,1195,1196],{"class":362},"    print",[218,1198,1199],{"class":270},"(item.name, item.size)\n",[203,1201,1203],{"id":1202},"step-4-share-a-read-only-link-that-expires","Step 4 — Share a read-only link that expires",[10,1205,1206],{},"The container should stay private. To give someone a download link, create a shared access signature for the single blob, with read permission only and an expiry:",[208,1208,1210],{"className":257,"code":1209,"language":259,"meta":213,"style":213},"import datetime as dt\nfrom azure.storage.blob import BlobSasPermissions, generate_blob_sas\n\ndef share_link(service, blob, days=7):\n    expiry = dt.datetime.now(dt.timezone.utc) + dt.timedelta(days=days)\n    sas = generate_blob_sas(\n        account_name=service.account_name,\n        container_name=blob.container_name,\n        blob_name=blob.blob_name,\n        account_key=service.credential.account_key,         # connection-string sign-in\n        permission=BlobSasPermissions(read=True),\n        expiry=expiry,\n    )\n    return f\"{blob.url}?{sas}\"\n",[215,1211,1212,1224,1235,1239,1256,1280,1290,1300,1310,1320,1333,1353,1363,1367],{"__ignoreMap":213},[218,1213,1214,1216,1219,1221],{"class":80,"line":220},[218,1215,267],{"class":266},[218,1217,1218],{"class":270}," datetime ",[218,1220,297],{"class":266},[218,1222,1223],{"class":270}," dt\n",[218,1225,1226,1228,1230,1232],{"class":80,"line":274},[218,1227,434],{"class":266},[218,1229,449],{"class":270},[218,1231,267],{"class":266},[218,1233,1234],{"class":270}," BlobSasPermissions, generate_blob_sas\n",[218,1236,1237],{"class":80,"line":282},[218,1238,286],{"emptyLinePlaceholder":285},[218,1240,1241,1243,1246,1249,1251,1254],{"class":80,"line":289},[218,1242,607],{"class":266},[218,1244,1245],{"class":610}," share_link",[218,1247,1248],{"class":270},"(service, blob, days",[218,1250,314],{"class":266},[218,1252,1253],{"class":362},"7",[218,1255,622],{"class":270},[218,1257,1258,1261,1263,1266,1269,1272,1275,1277],{"class":80,"line":303},[218,1259,1260],{"class":270},"    expiry ",[218,1262,314],{"class":266},[218,1264,1265],{"class":270}," dt.datetime.now(dt.timezone.utc) ",[218,1267,1268],{"class":266},"+",[218,1270,1271],{"class":270}," dt.timedelta(",[218,1273,1274],{"class":384},"days",[218,1276,314],{"class":266},[218,1278,1279],{"class":270},"days)\n",[218,1281,1282,1285,1287],{"class":80,"line":308},[218,1283,1284],{"class":270},"    sas ",[218,1286,314],{"class":266},[218,1288,1289],{"class":270}," generate_blob_sas(\n",[218,1291,1292,1295,1297],{"class":80,"line":320},[218,1293,1294],{"class":384},"        account_name",[218,1296,314],{"class":266},[218,1298,1299],{"class":270},"service.account_name,\n",[218,1301,1302,1305,1307],{"class":80,"line":354},[218,1303,1304],{"class":384},"        container_name",[218,1306,314],{"class":266},[218,1308,1309],{"class":270},"blob.container_name,\n",[218,1311,1312,1315,1317],{"class":80,"line":396},[218,1313,1314],{"class":384},"        blob_name",[218,1316,314],{"class":266},[218,1318,1319],{"class":270},"blob.blob_name,\n",[218,1321,1322,1325,1327,1330],{"class":80,"line":727},[218,1323,1324],{"class":384},"        account_key",[218,1326,314],{"class":266},[218,1328,1329],{"class":270},"service.credential.account_key,         ",[218,1331,1332],{"class":940},"# connection-string sign-in\n",[218,1334,1335,1338,1340,1343,1346,1348,1350],{"class":80,"line":733},[218,1336,1337],{"class":384},"        permission",[218,1339,314],{"class":266},[218,1341,1342],{"class":270},"BlobSasPermissions(",[218,1344,1345],{"class":384},"read",[218,1347,314],{"class":266},[218,1349,1139],{"class":362},[218,1351,1352],{"class":270},"),\n",[218,1354,1355,1358,1360],{"class":80,"line":745},[218,1356,1357],{"class":384},"        expiry",[218,1359,314],{"class":266},[218,1361,1362],{"class":270},"expiry,\n",[218,1364,1365],{"class":80,"line":756},[218,1366,823],{"class":270},[218,1368,1369,1371,1373,1375,1377,1380,1382,1385,1387,1390,1392],{"class":80,"line":768},[218,1370,901],{"class":266},[218,1372,632],{"class":266},[218,1374,635],{"class":227},[218,1376,639],{"class":638},[218,1378,1379],{"class":270},"blob.url",[218,1381,645],{"class":638},[218,1383,1384],{"class":227},"?",[218,1386,639],{"class":638},[218,1388,1389],{"class":270},"sas",[218,1391,645],{"class":638},[218,1393,1394],{"class":227},"\"\n",[10,1396,1397,1398,1401,1402,1404,1405,1408,1409,1412,1413,966],{},"Downloading that link against Azurite returned the workbook with status 200, the spreadsheet content type and the ",[215,1399,1400],{},"attachment; filename=\"sales_2026-09.xlsx\""," header set in Step 2. With ",[215,1403,414],{}," there is no account key; request a user delegation key with ",[215,1406,1407],{},"service.get_user_delegation_key(start, expiry)"," and pass it as ",[215,1410,1411],{},"user_delegation_key="," instead. The link can then last at most seven days, and it stops working if the identity loses access. Posting the link to a channel is covered in ",[14,1414,1416],{"href":1415},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fpost-an-excel-report-to-microsoft-teams-with-python\u002F","Post an Excel Report to Microsoft Teams with Python",[203,1418,1420],{"id":1419},"step-5-read-it-back","Step 5 — Read it back",[10,1422,1423],{},"A final check that the published file is the report you meant — the right size, a readable workbook, the right checksum:",[208,1425,1427],{"className":257,"code":1426,"language":259,"meta":213,"style":213},"properties = blob.get_blob_properties()\ndownloaded = blob.download_blob().readall()\nassert hashlib.sha256(downloaded).hexdigest() == properties.metadata[\"sha256\"]\nprint(properties.size, properties.content_settings.content_type, pd.read_excel(io.BytesIO(downloaded)).shape)\n# 5364 application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet (4, 2)\n",[215,1428,1429,1439,1449,1468,1475],{"__ignoreMap":213},[218,1430,1431,1434,1436],{"class":80,"line":220},[218,1432,1433],{"class":270},"properties ",[218,1435,314],{"class":266},[218,1437,1438],{"class":270}," blob.get_blob_properties()\n",[218,1440,1441,1444,1446],{"class":80,"line":274},[218,1442,1443],{"class":270},"downloaded ",[218,1445,314],{"class":266},[218,1447,1448],{"class":270}," blob.download_blob().readall()\n",[218,1450,1451,1454,1457,1460,1463,1465],{"class":80,"line":282},[218,1452,1453],{"class":266},"assert",[218,1455,1456],{"class":270}," hashlib.sha256(downloaded).hexdigest() ",[218,1458,1459],{"class":266},"==",[218,1461,1462],{"class":270}," properties.metadata[",[218,1464,814],{"class":227},[218,1466,1467],{"class":270},"]\n",[218,1469,1470,1472],{"class":80,"line":289},[218,1471,931],{"class":362},[218,1473,1474],{"class":270},"(properties.size, properties.content_settings.content_type, pd.read_excel(io.BytesIO(downloaded)).shape)\n",[218,1476,1477],{"class":80,"line":303},[218,1478,1479],{"class":940},"# 5364 application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet (4, 2)\n",[10,1481,1482],{},"Against Azurite this printed the workbook's 5,364 bytes, the spreadsheet content type and the expected shape. Reading reports back into pandas directly from storage is also how downstream jobs consume them.",[203,1484,1486],{"id":1485},"step-6-keep-the-archive-cheap-and-recoverable","Step 6 — Keep the archive cheap and recoverable",[10,1488,1489,1490,1493],{},"Monthly reports are read heavily for a few weeks and rarely afterwards. Blobs have access tiers: Hot costs more to store and less to read, Cool and Cold the reverse. Move older reports down a tier from the script, or better, with a lifecycle management rule on the account that does it for everything under ",[215,1491,1492],{},"monthly\u002F"," after 30 days:",[208,1495,1497],{"className":257,"code":1496,"language":259,"meta":213,"style":213},"from azure.storage.blob import StandardBlobTier\n\nold = container.get_blob_client(\"monthly\u002F2025\u002F09\u002Fsales_2025-09.xlsx\")\nold.set_standard_blob_tier(StandardBlobTier.COOL)\nprint(old.get_blob_properties().blob_tier)\n# Cool\n",[215,1498,1499,1510,1514,1528,1538,1545],{"__ignoreMap":213},[218,1500,1501,1503,1505,1507],{"class":80,"line":220},[218,1502,434],{"class":266},[218,1504,449],{"class":270},[218,1506,267],{"class":266},[218,1508,1509],{"class":270}," StandardBlobTier\n",[218,1511,1512],{"class":80,"line":274},[218,1513,286],{"emptyLinePlaceholder":285},[218,1515,1516,1519,1521,1523,1526],{"class":80,"line":282},[218,1517,1518],{"class":270},"old ",[218,1520,314],{"class":266},[218,1522,667],{"class":270},[218,1524,1525],{"class":227},"\"monthly\u002F2025\u002F09\u002Fsales_2025-09.xlsx\"",[218,1527,393],{"class":270},[218,1529,1530,1533,1536],{"class":80,"line":289},[218,1531,1532],{"class":270},"old.set_standard_blob_tier(StandardBlobTier.",[218,1534,1535],{"class":362},"COOL",[218,1537,393],{"class":270},[218,1539,1540,1542],{"class":80,"line":303},[218,1541,931],{"class":362},[218,1543,1544],{"class":270},"(old.get_blob_properties().blob_tier)\n",[218,1546,1547],{"class":80,"line":308},[218,1548,1549],{"class":940},"# Cool\n",[10,1551,1552,1553,1556,1557,1560],{},"Two account settings protect against the mistakes code cannot prevent. ",[421,1554,1555],{},"Soft delete"," keeps deleted blobs recoverable for a set number of days, and ",[421,1558,1559],{},"versioning"," keeps the previous content whenever a blob is overwritten — which makes the \"latest\" copy from Step 3 recoverable too. Both are switched on once in the portal or with infrastructure code, not per upload. For reports that must never change after publication, such as regulatory filings, a time-based immutability policy on the container makes even an account owner unable to alter them until it expires.",[20,1562,29,1568,29,1571,29,1574,29,1578,29,1583,29,1587,29,1592,29,1595,29,1599,29,1602,29,1606,29,1610,29,1615,29,1617,29,1621,29,1623,29,1626,29,1628,29,1631,29,1634,29,1637,29,1640,29,1644,29,1647,29,1650,29,1652,29,1656,29,1658,29,1660,29,1662,29,1664,29,1667,29,1671,29,1673,29,1677,29,1679,29,1682,29,1684],{"viewBox":1563,"role":23,"ariaLabelledBy":1564,"xmlns":27,"style":1567},"4 0 1138 174",[1565,1566],"az-auth-t","az-auth-d","width:100%;max-width:1138px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1569,1570],{"id":1565},"Choosing how the script signs in",[35,1572,1573],{"id":1566},"A managed identity suits scripts running inside Azure and needs no secret. A developer login through az login suits local runs with the same code. A connection string with an account key suits Azurite and systems outside Azure, but it grants full access to the account and must be stored as a secret. SAS links for sharing need an account key or a user delegation key.",[39,1575],{"x":41,"y":42,"width":1576,"height":1577,"fill":45},"1138","174",[39,1579],{"x":48,"y":49,"width":1580,"height":1581,"rx":42,"fill":45,"stroke":1005,"style":1582},"140.9","40.0","stroke-width:1px",[39,1584],{"x":1585,"y":49,"width":1586,"height":1581,"rx":42,"fill":53,"stroke":54,"style":1040},"160.9","193.0",[68,1588,1591],{"x":1589,"y":1590,"style":999},"257.4","41.0","Where it fits",[39,1593],{"x":1594,"y":49,"width":1586,"height":1581,"rx":42,"fill":53,"stroke":54,"style":1040},"353.9",[68,1596,1598],{"x":1597,"y":1590,"style":999},"450.4","Secret to store",[39,1600],{"x":1601,"y":49,"width":1586,"height":1581,"rx":42,"fill":53,"stroke":54,"style":1040},"547.0",[68,1603,1605],{"x":1604,"y":1590,"style":999},"643.5","Share links with",[39,1607],{"x":48,"y":1608,"width":1580,"height":1609,"rx":42,"fill":1004,"stroke":1005,"style":1582},"56.0","34.0",[68,1611,1614],{"x":1612,"y":988,"style":1613},"32.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","managed identity",[39,1616],{"x":1585,"y":1608,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1618,1620],{"x":1589,"y":1619,"style":77},"77.0","VMs",[39,1622],{"x":1594,"y":1608,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1624,1625],{"x":1597,"y":1619,"style":77},"Functions",[39,1627],{"x":1601,"y":1608,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1629,1630],{"x":1604,"y":1619,"style":77},"containers",[39,1632],{"x":1633,"y":1608,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},"740.0",[68,1635,64],{"x":1636,"y":1619,"style":77},"836.5",[39,1638],{"x":1639,"y":1608,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},"933.0",[68,1641,1643],{"x":1642,"y":1619,"style":77},"1029.6","user delegation key",[39,1645],{"x":48,"y":1646,"width":1580,"height":1609,"rx":42,"fill":1004,"stroke":1005,"style":1582},"90.0",[68,1648,418],{"x":1612,"y":1649,"style":1613},"112.0",[39,1651],{"x":1585,"y":1646,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1653,1655],{"x":1589,"y":1654,"style":77},"111.0","developer machines",[39,1657],{"x":1594,"y":1646,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1659,64],{"x":1597,"y":1654,"style":77},[39,1661],{"x":1601,"y":1646,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1663,1643],{"x":1604,"y":1654,"style":77},[39,1665],{"x":48,"y":1666,"width":1580,"height":1609,"rx":42,"fill":1004,"stroke":1005,"style":1582},"124.0",[68,1668,1670],{"x":1612,"y":1669,"style":1613},"146.0","connection string",[39,1672],{"x":1585,"y":1666,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1674,1676],{"x":1589,"y":1675,"style":77},"145.0","Azurite, outside Azure",[39,1678],{"x":1594,"y":1666,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1680,1681],{"x":1597,"y":1675,"style":77},"full account key",[39,1683],{"x":1601,"y":1666,"width":1586,"height":1609,"rx":42,"fill":45,"stroke":1005,"style":1582},[68,1685,1686],{"x":1604,"y":1675,"style":77},"account key",[203,1688,1690],{"id":1689},"common-pitfalls","Common pitfalls",[1692,1693,1694,1713,1723,1729,1739],"ul",{},[1695,1696,1697,1700,1701,1704,1705,1708,1709,1712],"li",{},[421,1698,1699],{},"Wrong content type."," Without ",[215,1702,1703],{},"ContentSettings",", blobs are stored as ",[215,1706,1707],{},"application\u002Foctet-stream",", and some browsers save them without the ",[215,1710,1711],{},".xlsx"," extension.",[1695,1714,1715,1718,1719,1722],{},[421,1716,1717],{},"Role assigned, still 403."," Role assignments can take several minutes to apply. A fresh ",[215,1720,1721],{},"AuthorizationPermissionMismatch"," right after granting access usually clears on its own.",[1695,1724,1725,1728],{},[421,1726,1727],{},"Account keys in scripts."," A connection string grants full control of the whole storage account. Prefer identities; if you must use a key, keep it in a secret store and rotate it.",[1695,1730,1731,1734,1735,1738],{},[421,1732,1733],{},"A SAS start time of \"now\"."," If the machine's clock runs slightly ahead of Azure's, a link that starts now is rejected for its first few minutes. Leave the start time unset, as ",[215,1736,1737],{},"share_link"," does, or set it a few minutes in the past.",[1695,1740,1741,1744],{},[421,1742,1743],{},"Public containers for convenience."," Anonymous read on a container exposes every report in it, including ones uploaded later. Share individual blobs with SAS links instead.",[203,1746,1748],{"id":1747},"conclusion","Conclusion",[10,1750,1751,1752,1754,1755,1757],{},"Sign in with ",[215,1753,414],{}," and a data-plane role, name blobs by period, upload with the spreadsheet content type, a download file name, a SHA-256 in metadata and ",[215,1756,147],{},", compare the stored MD5, keep a separate \"latest\" copy for fixed links, and share single reports with expiring read-only SAS links.",[203,1759,1761],{"id":1760},"frequently-asked-questions","Frequently asked questions",[10,1763,1764,1767],{},[421,1765,1766],{},"How do I upload an Excel file to Azure Blob Storage in Python?","\nInstall azure-storage-blob, create a BlobServiceClient, get a blob client for a container and name, and call upload_blob with the file's bytes. Pass ContentSettings with the xlsx content type so browsers and Office treat the download as a spreadsheet.",[10,1769,1770,1773],{},[421,1771,1772],{},"How do I stop an upload from replacing an existing report?","\nCall upload_blob with overwrite=False. If a blob with that name already exists the service refuses and the SDK raises ResourceExistsError with \"The specified blob already exists\", leaving the published report untouched.",[10,1775,1776,1779],{},[421,1777,1778],{},"How do I share an uploaded Excel report without making the container public?","\nGenerate a shared access signature (SAS) for that one blob with read permission and an expiry, and send the blob URL with the SAS appended. Anyone with the link can download it until it expires; the container stays private.",[10,1781,1782,1785],{},[421,1783,1784],{},"Which role does a script need to upload blobs with Azure AD?","\nA data-plane role on the storage account or container, such as Storage Blob Data Contributor. Owner or Contributor on the resource alone manages the account but does not grant access to blob data through Azure AD.",[203,1787,1789],{"id":1788},"related","Related",[1692,1791,1792,1798,1805,1812],{},[1695,1793,1794,1795,1797],{},"Up one level: ",[14,1796,17],{"href":16}," — where finished reports go.",[1695,1799,1800,1804],{},[14,1801,1803],{"href":1802},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002F","Upload an Excel Report to Amazon S3 with boto3"," — the same job on AWS.",[1695,1806,1807,1811],{},[14,1808,1810],{"href":1809},"\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"," — when readers live in Microsoft 365.",[1695,1813,1814,1818],{},[14,1815,1817],{"href":1816},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-over-sftp\u002F","Upload an Excel Report over SFTP with Python"," — for partners who expect a file drop.",[1820,1821,1822],"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}",{"title":213,"searchDepth":274,"depth":274,"links":1824},[1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835],{"id":205,"depth":274,"text":206},{"id":407,"depth":274,"text":408},{"id":547,"depth":274,"text":548},{"id":1100,"depth":274,"text":1101},{"id":1202,"depth":274,"text":1203},{"id":1419,"depth":274,"text":1420},{"id":1485,"depth":274,"text":1486},{"id":1689,"depth":274,"text":1690},{"id":1747,"depth":274,"text":1748},{"id":1760,"depth":274,"text":1761},{"id":1788,"depth":274,"text":1789},"2026-09-23","Publish generated Excel reports to Azure Blob Storage with the azure-storage-blob SDK — sign in without keys in the code, name blobs by period, set the content type and download file name, refuse to overwrite a published report, record a checksum in metadata, verify the upload and share it with a read-only link that expires.","md",[1840,1842,1844,1846],{"q":1766,"a":1841},"Install azure-storage-blob, create a BlobServiceClient, get a blob client for a container and name, and call upload_blob with the file's bytes. Pass ContentSettings with the xlsx content type so browsers and Office treat the download as a spreadsheet.",{"q":1772,"a":1843},"Call upload_blob with overwrite=False. If a blob with that name already exists the service refuses and the SDK raises ResourceExistsError with \"The specified blob already exists\", leaving the published report untouched.",{"q":1778,"a":1845},"Generate a shared access signature (SAS) for that one blob with read permission and an expiry, and send the blob URL with the SAS appended. Anyone with the link can download it until it expires; the container stays private.",{"q":1784,"a":1847},"A data-plane role on the storage account or container, such as Storage Blob Data Contributor. Owner or Contributor on the resource alone manages the account but does not grant access to blob data through Azure AD.","upload an excel report to azure blob storage with python",null,{"breadcrumb":1851},[1852,1854,1857],{"name":1853,"item":688},"Home",{"name":1855,"item":1856},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage",{"title":1860,"description":1861},"Upload an Excel File to Azure Blob Storage with Python","Upload xlsx reports to Azure Blob Storage from Python — DefaultAzureCredential or a connection string, ContentSettings with the spreadsheet MIME type and Content-Disposition, overwrite=False and ResourceExistsError, metadata with SHA-256, Content-MD5 checks, listing by prefix and read-only SAS links. Tested against Azurite.","upload-an-excel-report-to-azure-blob-storage","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-azure-blob-storage\u002Findex",[1865],"publishing","how-to","TqCv9URmWmV--gFyRrJNJvjM0GgD_fT2fIK-hNk20iA",[1869,1872],{"title":1803,"path":1870,"stem":1871,"children":-1},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-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",{"title":1873,"path":1874,"stem":1875,"children":-1},"Upload an Excel Report to Dropbox with Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-dropbox","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-dropbox\u002Findex",[1877,1879],{"path":1878,"title":1855},"\u002Fautomating-reporting-workflows",{"path":1880,"title":17},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage",[],[1883,1886,1889,1892,1895,1898],{"path":1884,"title":1885,"kind":1866},"\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":1887,"title":1888,"kind":1866},"\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":1890,"title":1891,"kind":1866},"\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":1893,"title":1894,"kind":1866},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses","Send an Excel Report with Amazon SES",{"path":1896,"title":1897,"kind":1866},"\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":1899,"title":1900,"kind":1866},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports","Name, Version and Archive Generated Excel Reports",1790972138474]