[{"data":1,"prerenderedAt":3355},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python":3347},{"id":4,"title":5,"body":6,"dateModified":3323,"datePublished":3323,"description":3324,"extension":3325,"faq":3326,"meta":3338,"navigation":392,"path":3339,"seo":3340,"slug":3343,"stem":3344,"type":3345,"__hash__":3346},"docs\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python\u002Findex.md","Upload an Excel Report to SharePoint with Python",{"type":7,"value":8,"toc":3310},"minimark",[9,19,179,184,215,223,272,281,352,356,359,862,869,876,880,883,1278,1286,1290,1293,1715,1718,1722,1725,1814,2358,2377,2384,2388,2395,2592,2603,2607,2745,2749,2832,2838,3012,3027,3172,3182,3194,3198,3207,3211,3226,3238,3244,3258,3264,3268,3306],[10,11,12,13,18],"p",{},"SharePoint and OneDrive are where most office workers already look for documents, which makes them the right destination for a recurring report inside a Microsoft-based organisation. The route from Python is Microsoft Graph: get an app-only token, resolve the site and drive, and PUT the file. The details that decide whether it works unattended are the permission model, the difference between a simple upload and a resumable session, and getting a shareable link back. This guide covers all of it. It is the Microsoft 365 path from ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",".",[20,21,30,31,30,35,30,39,30,46,30,56,30,63,30,68,30,73,30,77,30,83,30,88,30,93,30,97,30,101,30,104,30,107,30,111,30,115,30,120,30,124,30,128,30,131,30,134,30,138,30,142,30,148,30,152,30,157,30,160,30,168,30,174],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 262","img","Graph upload sequence: the job exchanges client credentials for a token, resolves the site and its drive, then uploads the file either directly or through a resumable session.",[26,27],"graph-t","graph-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"The four Graph calls behind a SharePoint upload",[36,37,38],"desc",{"id":27},"A sequence of four steps. First the job posts its client ID and secret to the Entra token endpoint and receives an app-only access token. Second it resolves the site by hostname and path to a site ID. Third it looks up that site's default document library drive. Fourth it uploads, choosing a direct PUT for small files or a resumable upload session for large ones. Each step depends on the identifier returned by the previous one.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","262","#ffffff",[40,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"14","52","176","86","13","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,62],"text",{"x":59,"y":60,"style":61},"102","38","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","1 · authenticate",[57,64,67],{"x":59,"y":65,"style":66},"84","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","client credentials",[57,69,72],{"x":59,"y":70,"style":71},"106","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","POST to the token",[57,74,76],{"x":59,"y":75,"style":71},"124","endpoint → access token",[78,79],"line",{"x1":80,"y1":81,"x2":82,"y2":81,"stroke":54,"style":55},"190","95","218",[84,85],"polygon",{"points":86,"fill":87},"226,95 214,89 214,101","#5b5cf0",[40,89],{"x":90,"y":49,"width":50,"height":51,"rx":52,"fill":91,"stroke":92,"style":55},"234","#fdefd8","var(--gold,#b4740a)",[57,94,96],{"x":95,"y":60,"style":61},"322","2 · resolve site",[57,98,100],{"x":95,"y":65,"style":99},"font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","site ID",[57,102,103],{"x":95,"y":70,"style":71},"by hostname",[57,105,106],{"x":95,"y":75,"style":71},"and server-relative path",[78,108],{"x1":109,"y1":81,"x2":110,"y2":81,"stroke":92,"style":55},"410","438",[84,112],{"points":113,"fill":114},"446,95 434,89 434,101","#b4740a",[40,116],{"x":117,"y":49,"width":50,"height":51,"rx":52,"fill":118,"stroke":119,"style":55},"454","#d9f4f1","var(--teal,#0f9488)",[57,121,123],{"x":122,"y":60,"style":61},"542","3 · resolve drive",[57,125,127],{"x":122,"y":65,"style":126},"font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","drive ID",[57,129,130],{"x":122,"y":70,"style":71},"the document library",[57,132,133],{"x":122,"y":75,"style":71},"behind the site",[78,135],{"x1":136,"y1":81,"x2":137,"y2":81,"stroke":119,"style":55},"630","658",[84,139],{"points":140,"fill":141},"666,95 654,89 654,101","#0f9488",[40,143],{"x":144,"y":49,"width":145,"height":51,"rx":52,"fill":146,"stroke":147,"style":55},"672","112","#f0f2f5","var(--line,#cdd5e6)",[57,149,151],{"x":150,"y":60,"style":61},"728","4 · upload",[57,153,156],{"x":150,"y":154,"style":155},"90","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","PUT",[57,158,159],{"x":150,"y":145,"style":155},"or session",[40,161],{"x":162,"y":50,"width":163,"height":164,"rx":165,"fill":166,"stroke":167,"style":55},"120","560","62","12","#fee8f2","var(--accent,#f43f8f)",[57,169,173],{"x":170,"y":171,"style":172},"400","200","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","a token is authentication; 403 means the consent or site grant is missing",[57,175,178],{"x":170,"y":176,"style":177},"222","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","application permissions need admin consent, and Sites.Selected needs a per-site grant",[180,181,183],"h2",{"id":182},"prerequisites","Prerequisites",[185,186,191],"pre",{"className":187,"code":188,"language":189,"meta":190,"style":190},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install requests pandas xlsxwriter\n","bash","",[192,193,194],"code",{"__ignoreMap":190},[195,196,198,202,206,209,212],"span",{"class":78,"line":197},1,[195,199,201],{"class":200},"sMTad","pip",[195,203,205],{"class":204},"srMev"," install",[195,207,208],{"class":204}," requests",[195,210,211],{"class":204}," pandas",[195,213,214],{"class":204}," xlsxwriter\n",[10,216,217,218,222],{},"An ",[219,220,221],"strong",{},"app registration"," in Microsoft Entra ID with a client secret, and one of two application permissions granted with admin consent:",[224,225,226,242],"table",{},[227,228,229],"thead",{},[230,231,232,236,239],"tr",{},[233,234,235],"th",{},"Permission",[233,237,238],{},"Scope",[233,240,241],{},"When to use",[243,244,245,259],"tbody",{},[230,246,247,253,256],{},[248,249,250],"td",{},[192,251,252],{},"Sites.Selected",[248,254,255],{},"only sites explicitly granted",[248,257,258],{},"the right default — least privilege",[230,260,261,266,269],{},[248,262,263],{},[192,264,265],{},"Files.ReadWrite.All",[248,267,268],{},"every site in the tenant",[248,270,271],{},"simpler, far broader than a report job needs",[10,273,274,276,277,280],{},[192,275,252],{}," needs a second step that catches people out: after consenting to the permission, an administrator must grant the app access to the specific site. Without that grant every call returns ",[192,278,279],{},"403"," despite a perfectly valid token.",[185,282,284],{"className":187,"code":283,"language":189,"meta":190,"style":190},"export GRAPH_TENANT_ID=...\nexport GRAPH_CLIENT_ID=...\nexport GRAPH_CLIENT_SECRET=...\nexport SP_HOSTNAME=contoso.sharepoint.com\nexport SP_SITE_PATH=\u002Fsites\u002Ffinance\n",[192,285,286,302,314,326,339],{"__ignoreMap":190},[195,287,288,292,296,299],{"class":78,"line":197},[195,289,291],{"class":290},"s-kum","export",[195,293,295],{"class":294},"skGVy"," GRAPH_TENANT_ID",[195,297,298],{"class":290},"=",[195,300,301],{"class":294},"...\n",[195,303,305,307,310,312],{"class":78,"line":304},2,[195,306,291],{"class":290},[195,308,309],{"class":294}," GRAPH_CLIENT_ID",[195,311,298],{"class":290},[195,313,301],{"class":294},[195,315,317,319,322,324],{"class":78,"line":316},3,[195,318,291],{"class":290},[195,320,321],{"class":294}," GRAPH_CLIENT_SECRET",[195,323,298],{"class":290},[195,325,301],{"class":294},[195,327,329,331,334,336],{"class":78,"line":328},4,[195,330,291],{"class":290},[195,332,333],{"class":294}," SP_HOSTNAME",[195,335,298],{"class":290},[195,337,338],{"class":294},"contoso.sharepoint.com\n",[195,340,342,344,347,349],{"class":78,"line":341},5,[195,343,291],{"class":290},[195,345,346],{"class":294}," SP_SITE_PATH",[195,348,298],{"class":290},[195,350,351],{"class":294},"\u002Fsites\u002Ffinance\n",[180,353,355],{"id":354},"step-1-get-an-app-only-token","Step 1 — Get an app-only token",[10,357,358],{},"The client-credentials flow is a single POST. No user, no interactive prompt, nothing that breaks when somebody changes their password:",[185,360,364],{"className":361,"code":362,"language":363,"meta":190,"style":190},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nimport time\nimport requests\n\nTOKEN_URL = \"https:\u002F\u002Flogin.microsoftonline.com\u002F{tenant}\u002Foauth2\u002Fv2.0\u002Ftoken\"\n\nclass GraphClient:\n    def __init__(self):\n        self.tenant = os.environ[\"GRAPH_TENANT_ID\"]\n        self.client_id = os.environ[\"GRAPH_CLIENT_ID\"]\n        self.client_secret = os.environ[\"GRAPH_CLIENT_SECRET\"]\n        self._token = None\n        self._expires_at = 0.0\n        self.session = requests.Session()\n\n    def token(self):\n        \"\"\"Return a valid token, refreshing a minute before it expires.\"\"\"\n        if self._token and time.time() \u003C self._expires_at - 60:\n            return self._token\n\n        response = self.session.post(\n            TOKEN_URL.format(tenant=self.tenant),\n            data={\n                \"client_id\": self.client_id,\n                \"client_secret\": self.client_secret,\n                \"scope\": \"https:\u002F\u002Fgraph.microsoft.com\u002F.default\",\n                \"grant_type\": \"client_credentials\",\n            },\n            timeout=30,\n        )\n        response.raise_for_status()\n        payload = response.json()\n\n        self._token = payload[\"access_token\"]\n        self._expires_at = time.time() + payload[\"expires_in\"]\n        return self._token\n\n    def headers(self, **extra):\n        return {\"Authorization\": f\"Bearer {self.token()}\", **extra}\n","python",[192,365,366,374,381,388,394,413,418,430,442,462,479,496,509,522,535,540,551,557,589,600,605,618,639,650,664,677,691,704,710,723,729,735,746,751,768,789,799,804,821],{"__ignoreMap":190},[195,367,368,371],{"class":78,"line":197},[195,369,370],{"class":290},"import",[195,372,373],{"class":294}," os\n",[195,375,376,378],{"class":78,"line":304},[195,377,370],{"class":290},[195,379,380],{"class":294}," time\n",[195,382,383,385],{"class":78,"line":316},[195,384,370],{"class":290},[195,386,387],{"class":294}," requests\n",[195,389,390],{"class":78,"line":328},[195,391,393],{"emptyLinePlaceholder":392},true,"\n",[195,395,396,400,403,406,410],{"class":78,"line":341},[195,397,399],{"class":398},"sP0c6","TOKEN_URL",[195,401,402],{"class":290}," =",[195,404,405],{"class":204}," \"https:\u002F\u002Flogin.microsoftonline.com\u002F",[195,407,409],{"class":408},"sSjpA","{tenant}",[195,411,412],{"class":204},"\u002Foauth2\u002Fv2.0\u002Ftoken\"\n",[195,414,416],{"class":78,"line":415},6,[195,417,393],{"emptyLinePlaceholder":392},[195,419,421,424,427],{"class":78,"line":420},7,[195,422,423],{"class":290},"class",[195,425,426],{"class":200}," GraphClient",[195,428,429],{"class":294},":\n",[195,431,433,436,439],{"class":78,"line":432},8,[195,434,435],{"class":290},"    def",[195,437,438],{"class":398}," __init__",[195,440,441],{"class":294},"(self):\n",[195,443,445,448,451,453,456,459],{"class":78,"line":444},9,[195,446,447],{"class":398},"        self",[195,449,450],{"class":294},".tenant ",[195,452,298],{"class":290},[195,454,455],{"class":294}," os.environ[",[195,457,458],{"class":204},"\"GRAPH_TENANT_ID\"",[195,460,461],{"class":294},"]\n",[195,463,465,467,470,472,474,477],{"class":78,"line":464},10,[195,466,447],{"class":398},[195,468,469],{"class":294},".client_id ",[195,471,298],{"class":290},[195,473,455],{"class":294},[195,475,476],{"class":204},"\"GRAPH_CLIENT_ID\"",[195,478,461],{"class":294},[195,480,482,484,487,489,491,494],{"class":78,"line":481},11,[195,483,447],{"class":398},[195,485,486],{"class":294},".client_secret ",[195,488,298],{"class":290},[195,490,455],{"class":294},[195,492,493],{"class":204},"\"GRAPH_CLIENT_SECRET\"",[195,495,461],{"class":294},[195,497,499,501,504,506],{"class":78,"line":498},12,[195,500,447],{"class":398},[195,502,503],{"class":294},"._token ",[195,505,298],{"class":290},[195,507,508],{"class":398}," None\n",[195,510,512,514,517,519],{"class":78,"line":511},13,[195,513,447],{"class":398},[195,515,516],{"class":294},"._expires_at ",[195,518,298],{"class":290},[195,520,521],{"class":398}," 0.0\n",[195,523,525,527,530,532],{"class":78,"line":524},14,[195,526,447],{"class":398},[195,528,529],{"class":294},".session ",[195,531,298],{"class":290},[195,533,534],{"class":294}," requests.Session()\n",[195,536,538],{"class":78,"line":537},15,[195,539,393],{"emptyLinePlaceholder":392},[195,541,543,545,549],{"class":78,"line":542},16,[195,544,435],{"class":290},[195,546,548],{"class":547},"s_Opv"," token",[195,550,441],{"class":294},[195,552,554],{"class":78,"line":553},17,[195,555,556],{"class":204},"        \"\"\"Return a valid token, refreshing a minute before it expires.\"\"\"\n",[195,558,560,563,566,568,571,574,577,579,581,584,587],{"class":78,"line":559},18,[195,561,562],{"class":290},"        if",[195,564,565],{"class":398}," self",[195,567,503],{"class":294},[195,569,570],{"class":290},"and",[195,572,573],{"class":294}," time.time() ",[195,575,576],{"class":290},"\u003C",[195,578,565],{"class":398},[195,580,516],{"class":294},[195,582,583],{"class":290},"-",[195,585,586],{"class":398}," 60",[195,588,429],{"class":294},[195,590,592,595,597],{"class":78,"line":591},19,[195,593,594],{"class":290},"            return",[195,596,565],{"class":398},[195,598,599],{"class":294},"._token\n",[195,601,603],{"class":78,"line":602},20,[195,604,393],{"emptyLinePlaceholder":392},[195,606,608,611,613,615],{"class":78,"line":607},21,[195,609,610],{"class":294},"        response ",[195,612,298],{"class":290},[195,614,565],{"class":398},[195,616,617],{"class":294},".session.post(\n",[195,619,621,624,627,631,633,636],{"class":78,"line":620},22,[195,622,623],{"class":398},"            TOKEN_URL",[195,625,626],{"class":294},".format(",[195,628,630],{"class":629},"sa561","tenant",[195,632,298],{"class":290},[195,634,635],{"class":398},"self",[195,637,638],{"class":294},".tenant),\n",[195,640,642,645,647],{"class":78,"line":641},23,[195,643,644],{"class":629},"            data",[195,646,298],{"class":290},[195,648,649],{"class":294},"{\n",[195,651,653,656,659,661],{"class":78,"line":652},24,[195,654,655],{"class":204},"                \"client_id\"",[195,657,658],{"class":294},": ",[195,660,635],{"class":398},[195,662,663],{"class":294},".client_id,\n",[195,665,667,670,672,674],{"class":78,"line":666},25,[195,668,669],{"class":204},"                \"client_secret\"",[195,671,658],{"class":294},[195,673,635],{"class":398},[195,675,676],{"class":294},".client_secret,\n",[195,678,680,683,685,688],{"class":78,"line":679},26,[195,681,682],{"class":204},"                \"scope\"",[195,684,658],{"class":294},[195,686,687],{"class":204},"\"https:\u002F\u002Fgraph.microsoft.com\u002F.default\"",[195,689,690],{"class":294},",\n",[195,692,694,697,699,702],{"class":78,"line":693},27,[195,695,696],{"class":204},"                \"grant_type\"",[195,698,658],{"class":294},[195,700,701],{"class":204},"\"client_credentials\"",[195,703,690],{"class":294},[195,705,707],{"class":78,"line":706},28,[195,708,709],{"class":294},"            },\n",[195,711,713,716,718,721],{"class":78,"line":712},29,[195,714,715],{"class":629},"            timeout",[195,717,298],{"class":290},[195,719,720],{"class":398},"30",[195,722,690],{"class":294},[195,724,726],{"class":78,"line":725},30,[195,727,728],{"class":294},"        )\n",[195,730,732],{"class":78,"line":731},31,[195,733,734],{"class":294},"        response.raise_for_status()\n",[195,736,738,741,743],{"class":78,"line":737},32,[195,739,740],{"class":294},"        payload ",[195,742,298],{"class":290},[195,744,745],{"class":294}," response.json()\n",[195,747,749],{"class":78,"line":748},33,[195,750,393],{"emptyLinePlaceholder":392},[195,752,754,756,758,760,763,766],{"class":78,"line":753},34,[195,755,447],{"class":398},[195,757,503],{"class":294},[195,759,298],{"class":290},[195,761,762],{"class":294}," payload[",[195,764,765],{"class":204},"\"access_token\"",[195,767,461],{"class":294},[195,769,771,773,775,777,779,782,784,787],{"class":78,"line":770},35,[195,772,447],{"class":398},[195,774,516],{"class":294},[195,776,298],{"class":290},[195,778,573],{"class":294},[195,780,781],{"class":290},"+",[195,783,762],{"class":294},[195,785,786],{"class":204},"\"expires_in\"",[195,788,461],{"class":294},[195,790,792,795,797],{"class":78,"line":791},36,[195,793,794],{"class":290},"        return",[195,796,565],{"class":398},[195,798,599],{"class":294},[195,800,802],{"class":78,"line":801},37,[195,803,393],{"emptyLinePlaceholder":392},[195,805,807,809,812,815,818],{"class":78,"line":806},38,[195,808,435],{"class":290},[195,810,811],{"class":547}," headers",[195,813,814],{"class":294},"(self, ",[195,816,817],{"class":290},"**",[195,819,820],{"class":294},"extra):\n",[195,822,824,826,829,832,834,837,840,843,845,848,851,854,857,859],{"class":78,"line":823},39,[195,825,794],{"class":290},[195,827,828],{"class":294}," {",[195,830,831],{"class":204},"\"Authorization\"",[195,833,658],{"class":294},[195,835,836],{"class":290},"f",[195,838,839],{"class":204},"\"Bearer ",[195,841,842],{"class":408},"{",[195,844,635],{"class":398},[195,846,847],{"class":294},".token()",[195,849,850],{"class":408},"}",[195,852,853],{"class":204},"\"",[195,855,856],{"class":294},", ",[195,858,817],{"class":290},[195,860,861],{"class":294},"extra}\n",[10,863,864,865,868],{},"Caching the token matters. Graph tokens last around an hour, and a job that requests a fresh one per file will be throttled on any fan-out of more than a handful of reports. The ",[192,866,867],{},"- 60"," margin avoids the race where a token expires between the check and the request landing.",[10,870,871,872,875],{},"Note the scope: ",[192,873,874],{},".default"," with client credentials means \"every application permission this app has been consented for\", which is how app-only access works — you do not list individual scopes.",[180,877,879],{"id":878},"step-2-resolve-the-site-and-its-drive","Step 2 — Resolve the site and its drive",[10,881,882],{},"Graph addresses documents by drive and item, so a human-readable site URL has to be translated first:",[185,884,886],{"className":361,"code":885,"language":363,"meta":190,"style":190},"GRAPH = \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n\nclass GraphClient(GraphClient):        # continuing the class above\n    def site_id(self, hostname, site_path):\n        \"\"\"Resolve https:\u002F\u002F{hostname}{site_path} to a Graph site ID.\"\"\"\n        url = f\"{GRAPH}\u002Fsites\u002F{hostname}:{site_path}\"\n        r = self.session.get(url, headers=self.headers(), timeout=30)\n        r.raise_for_status()\n        return r.json()[\"id\"]\n\n    def drive_id(self, site_id, library=None):\n        \"\"\"The default document library, or a named one.\"\"\"\n        if library is None:\n            r = self.session.get(f\"{GRAPH}\u002Fsites\u002F{site_id}\u002Fdrive\",\n                                 headers=self.headers(), timeout=30)\n            r.raise_for_status()\n            return r.json()[\"id\"]\n\n        r = self.session.get(f\"{GRAPH}\u002Fsites\u002F{site_id}\u002Fdrives\",\n                             headers=self.headers(), timeout=30)\n        r.raise_for_status()\n        for drive in r.json()[\"value\"]:\n            if drive[\"name\"] == library:\n                return drive[\"id\"]\n        raise LookupError(f\"no document library named {library!r}\")\n",[192,887,888,898,902,922,932,937,978,1010,1015,1027,1031,1049,1054,1069,1105,1124,1129,1139,1143,1176,1195,1199,1218,1238,1249],{"__ignoreMap":190},[195,889,890,893,895],{"class":78,"line":197},[195,891,892],{"class":398},"GRAPH",[195,894,402],{"class":290},[195,896,897],{"class":204}," \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n",[195,899,900],{"class":78,"line":304},[195,901,393],{"emptyLinePlaceholder":392},[195,903,904,906,908,911,915,918],{"class":78,"line":316},[195,905,423],{"class":290},[195,907,426],{"class":200},[195,909,910],{"class":294},"(",[195,912,914],{"class":913},"sHg5J","GraphClient",[195,916,917],{"class":294},"):        ",[195,919,921],{"class":920},"s-wDw","# continuing the class above\n",[195,923,924,926,929],{"class":78,"line":328},[195,925,435],{"class":290},[195,927,928],{"class":547}," site_id",[195,930,931],{"class":294},"(self, hostname, site_path):\n",[195,933,934],{"class":78,"line":341},[195,935,936],{"class":204},"        \"\"\"Resolve https:\u002F\u002F{hostname}{site_path} to a Graph site ID.\"\"\"\n",[195,938,939,942,944,947,949,951,953,955,958,960,963,965,968,970,973,975],{"class":78,"line":415},[195,940,941],{"class":294},"        url ",[195,943,298],{"class":290},[195,945,946],{"class":290}," f",[195,948,853],{"class":204},[195,950,842],{"class":408},[195,952,892],{"class":398},[195,954,850],{"class":408},[195,956,957],{"class":204},"\u002Fsites\u002F",[195,959,842],{"class":408},[195,961,962],{"class":294},"hostname",[195,964,850],{"class":408},[195,966,967],{"class":204},":",[195,969,842],{"class":408},[195,971,972],{"class":294},"site_path",[195,974,850],{"class":408},[195,976,977],{"class":204},"\"\n",[195,979,980,983,985,987,990,993,995,997,1000,1003,1005,1007],{"class":78,"line":420},[195,981,982],{"class":294},"        r ",[195,984,298],{"class":290},[195,986,565],{"class":398},[195,988,989],{"class":294},".session.get(url, ",[195,991,992],{"class":629},"headers",[195,994,298],{"class":290},[195,996,635],{"class":398},[195,998,999],{"class":294},".headers(), ",[195,1001,1002],{"class":629},"timeout",[195,1004,298],{"class":290},[195,1006,720],{"class":398},[195,1008,1009],{"class":294},")\n",[195,1011,1012],{"class":78,"line":432},[195,1013,1014],{"class":294},"        r.raise_for_status()\n",[195,1016,1017,1019,1022,1025],{"class":78,"line":444},[195,1018,794],{"class":290},[195,1020,1021],{"class":294}," r.json()[",[195,1023,1024],{"class":204},"\"id\"",[195,1026,461],{"class":294},[195,1028,1029],{"class":78,"line":464},[195,1030,393],{"emptyLinePlaceholder":392},[195,1032,1033,1035,1038,1041,1043,1046],{"class":78,"line":481},[195,1034,435],{"class":290},[195,1036,1037],{"class":547}," drive_id",[195,1039,1040],{"class":294},"(self, site_id, library",[195,1042,298],{"class":290},[195,1044,1045],{"class":398},"None",[195,1047,1048],{"class":294},"):\n",[195,1050,1051],{"class":78,"line":498},[195,1052,1053],{"class":204},"        \"\"\"The default document library, or a named one.\"\"\"\n",[195,1055,1056,1058,1061,1064,1067],{"class":78,"line":511},[195,1057,562],{"class":290},[195,1059,1060],{"class":294}," library ",[195,1062,1063],{"class":290},"is",[195,1065,1066],{"class":398}," None",[195,1068,429],{"class":294},[195,1070,1071,1074,1076,1078,1081,1083,1085,1087,1089,1091,1093,1095,1098,1100,1103],{"class":78,"line":524},[195,1072,1073],{"class":294},"            r ",[195,1075,298],{"class":290},[195,1077,565],{"class":398},[195,1079,1080],{"class":294},".session.get(",[195,1082,836],{"class":290},[195,1084,853],{"class":204},[195,1086,842],{"class":408},[195,1088,892],{"class":398},[195,1090,850],{"class":408},[195,1092,957],{"class":204},[195,1094,842],{"class":408},[195,1096,1097],{"class":294},"site_id",[195,1099,850],{"class":408},[195,1101,1102],{"class":204},"\u002Fdrive\"",[195,1104,690],{"class":294},[195,1106,1107,1110,1112,1114,1116,1118,1120,1122],{"class":78,"line":537},[195,1108,1109],{"class":629},"                                 headers",[195,1111,298],{"class":290},[195,1113,635],{"class":398},[195,1115,999],{"class":294},[195,1117,1002],{"class":629},[195,1119,298],{"class":290},[195,1121,720],{"class":398},[195,1123,1009],{"class":294},[195,1125,1126],{"class":78,"line":542},[195,1127,1128],{"class":294},"            r.raise_for_status()\n",[195,1130,1131,1133,1135,1137],{"class":78,"line":553},[195,1132,594],{"class":290},[195,1134,1021],{"class":294},[195,1136,1024],{"class":204},[195,1138,461],{"class":294},[195,1140,1141],{"class":78,"line":559},[195,1142,393],{"emptyLinePlaceholder":392},[195,1144,1145,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1174],{"class":78,"line":591},[195,1146,982],{"class":294},[195,1148,298],{"class":290},[195,1150,565],{"class":398},[195,1152,1080],{"class":294},[195,1154,836],{"class":290},[195,1156,853],{"class":204},[195,1158,842],{"class":408},[195,1160,892],{"class":398},[195,1162,850],{"class":408},[195,1164,957],{"class":204},[195,1166,842],{"class":408},[195,1168,1097],{"class":294},[195,1170,850],{"class":408},[195,1172,1173],{"class":204},"\u002Fdrives\"",[195,1175,690],{"class":294},[195,1177,1178,1181,1183,1185,1187,1189,1191,1193],{"class":78,"line":602},[195,1179,1180],{"class":629},"                             headers",[195,1182,298],{"class":290},[195,1184,635],{"class":398},[195,1186,999],{"class":294},[195,1188,1002],{"class":629},[195,1190,298],{"class":290},[195,1192,720],{"class":398},[195,1194,1009],{"class":294},[195,1196,1197],{"class":78,"line":607},[195,1198,1014],{"class":294},[195,1200,1201,1204,1207,1210,1212,1215],{"class":78,"line":620},[195,1202,1203],{"class":290},"        for",[195,1205,1206],{"class":294}," drive ",[195,1208,1209],{"class":290},"in",[195,1211,1021],{"class":294},[195,1213,1214],{"class":204},"\"value\"",[195,1216,1217],{"class":294},"]:\n",[195,1219,1220,1223,1226,1229,1232,1235],{"class":78,"line":641},[195,1221,1222],{"class":290},"            if",[195,1224,1225],{"class":294}," drive[",[195,1227,1228],{"class":204},"\"name\"",[195,1230,1231],{"class":294},"] ",[195,1233,1234],{"class":290},"==",[195,1236,1237],{"class":294}," library:\n",[195,1239,1240,1243,1245,1247],{"class":78,"line":652},[195,1241,1242],{"class":290},"                return",[195,1244,1225],{"class":294},[195,1246,1024],{"class":204},[195,1248,461],{"class":294},[195,1250,1251,1254,1257,1259,1261,1264,1266,1269,1272,1274,1276],{"class":78,"line":666},[195,1252,1253],{"class":290},"        raise",[195,1255,1256],{"class":398}," LookupError",[195,1258,910],{"class":294},[195,1260,836],{"class":290},[195,1262,1263],{"class":204},"\"no document library named ",[195,1265,842],{"class":408},[195,1267,1268],{"class":294},"library",[195,1270,1271],{"class":290},"!r",[195,1273,850],{"class":408},[195,1275,853],{"class":204},[195,1277,1009],{"class":294},[10,1279,1280,1281,1285],{},"Resolve these once and cache the IDs — they do not change between runs, and looking them up on every upload wastes two round trips per file. Putting them in the job's configuration, as described in ",[14,1282,1284],{"href":1283},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","keeping Excel report settings in a config file",", is the tidy version.",[180,1287,1289],{"id":1288},"step-3-upload-a-small-report","Step 3 — Upload a small report",[10,1291,1292],{},"For anything up to a few megabytes, a direct PUT to the content endpoint is all it takes:",[185,1294,1296],{"className":361,"code":1295,"language":363,"meta":190,"style":190},"import io\nimport pandas as pd\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\nclass GraphClient(GraphClient):\n    def upload_small(self, drive_id, folder, filename, data):\n        \"\"\"PUT a file straight into a folder. Replaces any existing item.\"\"\"\n        path = f\"{folder.strip('\u002F')}\u002F{filename}\"\n        url = f\"{GRAPH}\u002Fdrives\u002F{drive_id}\u002Froot:\u002F{path}:\u002Fcontent\"\n\n        r = self.session.put(\n            url,\n            headers=self.headers(**{\"Content-Type\": XLSX}),\n            data=data,\n            timeout=120,\n        )\n        r.raise_for_status()\n        return r.json()          # the driveItem, including id and webUrl\n\ngraph = GraphClient()\nsite = graph.site_id(os.environ[\"SP_HOSTNAME\"], os.environ[\"SP_SITE_PATH\"])\ndrive = graph.drive_id(site)\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [159.92, 247.50]})\nbuffer = io.BytesIO()\nwith pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\n\nitem = graph.upload_small(drive, \"Reports\u002F2026\", \"regional-2026-08.xlsx\",\n                          buffer.getvalue())\nprint(item[\"webUrl\"])\n",[192,1297,1298,1305,1318,1322,1332,1336,1348,1358,1363,1399,1438,1442,1453,1458,1484,1493,1503,1507,1511,1521,1525,1535,1557,1567,1571,1614,1624,1648,1673,1677,1697,1702],{"__ignoreMap":190},[195,1299,1300,1302],{"class":78,"line":197},[195,1301,370],{"class":290},[195,1303,1304],{"class":294}," io\n",[195,1306,1307,1309,1312,1315],{"class":78,"line":304},[195,1308,370],{"class":290},[195,1310,1311],{"class":294}," pandas ",[195,1313,1314],{"class":290},"as",[195,1316,1317],{"class":294}," pd\n",[195,1319,1320],{"class":78,"line":316},[195,1321,393],{"emptyLinePlaceholder":392},[195,1323,1324,1327,1329],{"class":78,"line":328},[195,1325,1326],{"class":398},"XLSX",[195,1328,402],{"class":290},[195,1330,1331],{"class":204}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[195,1333,1334],{"class":78,"line":341},[195,1335,393],{"emptyLinePlaceholder":392},[195,1337,1338,1340,1342,1344,1346],{"class":78,"line":415},[195,1339,423],{"class":290},[195,1341,426],{"class":200},[195,1343,910],{"class":294},[195,1345,914],{"class":913},[195,1347,1048],{"class":294},[195,1349,1350,1352,1355],{"class":78,"line":420},[195,1351,435],{"class":290},[195,1353,1354],{"class":547}," upload_small",[195,1356,1357],{"class":294},"(self, drive_id, folder, filename, data):\n",[195,1359,1360],{"class":78,"line":432},[195,1361,1362],{"class":204},"        \"\"\"PUT a file straight into a folder. Replaces any existing item.\"\"\"\n",[195,1364,1365,1368,1370,1372,1374,1376,1379,1382,1385,1387,1390,1392,1395,1397],{"class":78,"line":444},[195,1366,1367],{"class":294},"        path ",[195,1369,298],{"class":290},[195,1371,946],{"class":290},[195,1373,853],{"class":204},[195,1375,842],{"class":408},[195,1377,1378],{"class":294},"folder.strip(",[195,1380,1381],{"class":204},"'\u002F'",[195,1383,1384],{"class":294},")",[195,1386,850],{"class":408},[195,1388,1389],{"class":204},"\u002F",[195,1391,842],{"class":408},[195,1393,1394],{"class":294},"filename",[195,1396,850],{"class":408},[195,1398,977],{"class":204},[195,1400,1401,1403,1405,1407,1409,1411,1413,1415,1418,1420,1423,1425,1428,1430,1433,1435],{"class":78,"line":464},[195,1402,941],{"class":294},[195,1404,298],{"class":290},[195,1406,946],{"class":290},[195,1408,853],{"class":204},[195,1410,842],{"class":408},[195,1412,892],{"class":398},[195,1414,850],{"class":408},[195,1416,1417],{"class":204},"\u002Fdrives\u002F",[195,1419,842],{"class":408},[195,1421,1422],{"class":294},"drive_id",[195,1424,850],{"class":408},[195,1426,1427],{"class":204},"\u002Froot:\u002F",[195,1429,842],{"class":408},[195,1431,1432],{"class":294},"path",[195,1434,850],{"class":408},[195,1436,1437],{"class":204},":\u002Fcontent\"\n",[195,1439,1440],{"class":78,"line":481},[195,1441,393],{"emptyLinePlaceholder":392},[195,1443,1444,1446,1448,1450],{"class":78,"line":498},[195,1445,982],{"class":294},[195,1447,298],{"class":290},[195,1449,565],{"class":398},[195,1451,1452],{"class":294},".session.put(\n",[195,1454,1455],{"class":78,"line":511},[195,1456,1457],{"class":294},"            url,\n",[195,1459,1460,1463,1465,1467,1470,1472,1474,1477,1479,1481],{"class":78,"line":524},[195,1461,1462],{"class":629},"            headers",[195,1464,298],{"class":290},[195,1466,635],{"class":398},[195,1468,1469],{"class":294},".headers(",[195,1471,817],{"class":290},[195,1473,842],{"class":294},[195,1475,1476],{"class":204},"\"Content-Type\"",[195,1478,658],{"class":294},[195,1480,1326],{"class":398},[195,1482,1483],{"class":294},"}),\n",[195,1485,1486,1488,1490],{"class":78,"line":537},[195,1487,644],{"class":629},[195,1489,298],{"class":290},[195,1491,1492],{"class":294},"data,\n",[195,1494,1495,1497,1499,1501],{"class":78,"line":542},[195,1496,715],{"class":629},[195,1498,298],{"class":290},[195,1500,162],{"class":398},[195,1502,690],{"class":294},[195,1504,1505],{"class":78,"line":553},[195,1506,728],{"class":294},[195,1508,1509],{"class":78,"line":559},[195,1510,1014],{"class":294},[195,1512,1513,1515,1518],{"class":78,"line":591},[195,1514,794],{"class":290},[195,1516,1517],{"class":294}," r.json()          ",[195,1519,1520],{"class":920},"# the driveItem, including id and webUrl\n",[195,1522,1523],{"class":78,"line":602},[195,1524,393],{"emptyLinePlaceholder":392},[195,1526,1527,1530,1532],{"class":78,"line":607},[195,1528,1529],{"class":294},"graph ",[195,1531,298],{"class":290},[195,1533,1534],{"class":294}," GraphClient()\n",[195,1536,1537,1540,1542,1545,1548,1551,1554],{"class":78,"line":620},[195,1538,1539],{"class":294},"site ",[195,1541,298],{"class":290},[195,1543,1544],{"class":294}," graph.site_id(os.environ[",[195,1546,1547],{"class":204},"\"SP_HOSTNAME\"",[195,1549,1550],{"class":294},"], os.environ[",[195,1552,1553],{"class":204},"\"SP_SITE_PATH\"",[195,1555,1556],{"class":294},"])\n",[195,1558,1559,1562,1564],{"class":78,"line":641},[195,1560,1561],{"class":294},"drive ",[195,1563,298],{"class":290},[195,1565,1566],{"class":294}," graph.drive_id(site)\n",[195,1568,1569],{"class":78,"line":652},[195,1570,393],{"emptyLinePlaceholder":392},[195,1572,1573,1576,1578,1581,1584,1587,1590,1592,1595,1598,1601,1603,1606,1608,1611],{"class":78,"line":666},[195,1574,1575],{"class":294},"df ",[195,1577,298],{"class":290},[195,1579,1580],{"class":294}," pd.DataFrame({",[195,1582,1583],{"class":204},"\"region\"",[195,1585,1586],{"class":294},": [",[195,1588,1589],{"class":204},"\"North\"",[195,1591,856],{"class":294},[195,1593,1594],{"class":204},"\"South\"",[195,1596,1597],{"class":294},"], ",[195,1599,1600],{"class":204},"\"revenue\"",[195,1602,1586],{"class":294},[195,1604,1605],{"class":398},"159.92",[195,1607,856],{"class":294},[195,1609,1610],{"class":398},"247.50",[195,1612,1613],{"class":294},"]})\n",[195,1615,1616,1619,1621],{"class":78,"line":679},[195,1617,1618],{"class":294},"buffer ",[195,1620,298],{"class":290},[195,1622,1623],{"class":294}," io.BytesIO()\n",[195,1625,1626,1629,1632,1635,1637,1640,1643,1645],{"class":78,"line":693},[195,1627,1628],{"class":290},"with",[195,1630,1631],{"class":294}," pd.ExcelWriter(buffer, ",[195,1633,1634],{"class":629},"engine",[195,1636,298],{"class":290},[195,1638,1639],{"class":204},"\"xlsxwriter\"",[195,1641,1642],{"class":294},") ",[195,1644,1314],{"class":290},[195,1646,1647],{"class":294}," writer:\n",[195,1649,1650,1653,1656,1658,1661,1663,1666,1668,1671],{"class":78,"line":706},[195,1651,1652],{"class":294},"    df.to_excel(writer, ",[195,1654,1655],{"class":629},"sheet_name",[195,1657,298],{"class":290},[195,1659,1660],{"class":204},"\"Summary\"",[195,1662,856],{"class":294},[195,1664,1665],{"class":629},"index",[195,1667,298],{"class":290},[195,1669,1670],{"class":398},"False",[195,1672,1009],{"class":294},[195,1674,1675],{"class":78,"line":712},[195,1676,393],{"emptyLinePlaceholder":392},[195,1678,1679,1682,1684,1687,1690,1692,1695],{"class":78,"line":725},[195,1680,1681],{"class":294},"item ",[195,1683,298],{"class":290},[195,1685,1686],{"class":294}," graph.upload_small(drive, ",[195,1688,1689],{"class":204},"\"Reports\u002F2026\"",[195,1691,856],{"class":294},[195,1693,1694],{"class":204},"\"regional-2026-08.xlsx\"",[195,1696,690],{"class":294},[195,1698,1699],{"class":78,"line":731},[195,1700,1701],{"class":294},"                          buffer.getvalue())\n",[195,1703,1704,1707,1710,1713],{"class":78,"line":737},[195,1705,1706],{"class":398},"print",[195,1708,1709],{"class":294},"(item[",[195,1711,1712],{"class":204},"\"webUrl\"",[195,1714,1556],{"class":294},[10,1716,1717],{},"A PUT to an existing path replaces the file and SharePoint keeps the previous version in its history — which is exactly the behaviour a monthly report wants. No staging dance is needed here: SharePoint does not expose a partially uploaded item, so the write is effectively atomic already.",[180,1719,1721],{"id":1720},"step-4-upload-a-large-report-resumably","Step 4 — Upload a large report resumably",[10,1723,1724],{},"Above a few megabytes, use an upload session. It chunks the transfer and, crucially, survives a dropped connection:",[20,1726,30,1732,30,1735,30,1738,30,1741,30,1746,30,1751,30,1756,30,1761,30,1764,30,1768,30,1771,30,1774,30,1778,30,1781,30,1785,30,1789,30,1792,30,1796,30,1800,30,1806,30,1810],{"viewBox":1727,"role":23,"ariaLabel":1728,"ariaLabelledBy":1729,"xmlns":28,"style":29},"0 0 800 226","Resumable upload session: the job creates a session, sends fixed-size chunks with Content-Range headers, and Graph reports which byte ranges it still expects so a dropped connection resumes rather than restarting.",[1730,1731],"sess-t","sess-d",[32,1733,1734],{"id":1730},"How an upload session survives a dropped connection",[36,1736,1737],{"id":1731},"A file is split into four chunks. Chunks one and two upload successfully and Graph returns a 202 with the byte ranges it still expects. Chunk three fails on a network drop. Because the session records progress, the job resumes from the start of chunk three rather than from the beginning of the file. The final chunk returns 201 with the completed driveItem.",[40,1739],{"x":42,"y":42,"width":43,"height":1740,"fill":45},"226",[57,1742,1745],{"x":170,"y":1743,"style":1744},"26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the session remembers what arrived",[40,1747],{"x":720,"y":1748,"width":1749,"height":49,"rx":1750,"fill":118,"stroke":119,"style":55},"46","168","9",[57,1752,1755],{"x":1753,"y":1754,"style":126},"114","68","chunk 1 → 202",[57,1757,1760],{"x":1753,"y":1758,"style":1759},"87","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","bytes 0–10485759",[40,1762],{"x":1763,"y":1748,"width":1749,"height":49,"rx":1750,"fill":118,"stroke":119,"style":55},"214",[57,1765,1767],{"x":1766,"y":1754,"style":126},"298","chunk 2 → 202",[57,1769,1770],{"x":1766,"y":1758,"style":1759},"next expected range returned",[40,1772],{"x":1773,"y":1748,"width":1749,"height":49,"rx":1750,"fill":166,"stroke":167,"style":55},"398",[57,1775,1777],{"x":1776,"y":1754,"style":172},"482","chunk 3 → dropped",[57,1779,1780],{"x":1776,"y":1758,"style":1759},"connection lost",[40,1782],{"x":1783,"y":1748,"width":1784,"height":49,"rx":1750,"fill":53,"stroke":54,"style":55},"582","188",[57,1786,1788],{"x":1787,"y":1754,"style":66},"676","chunk 4 → 201",[57,1790,1791],{"x":1787,"y":1758,"style":1759},"driveItem returned",[1432,1793],{"d":1794,"fill":1795,"stroke":167,"style":55},"M482 106 V 132 H 398 V 152","none",[84,1797],{"points":1798,"fill":1799},"398,160 391,148 405,148","#f43f8f",[40,1801],{"x":1802,"y":1803,"width":1804,"height":1748,"rx":1805,"fill":91,"stroke":92,"style":55},"240","160","320","10",[57,1807,1809],{"x":170,"y":1808,"style":99},"180","resume from chunk 3",[57,1811,1813],{"x":170,"y":1812,"style":177},"198","chunks 1 and 2 are not re-sent",[185,1815,1817],{"className":361,"code":1816,"language":363,"meta":190,"style":190},"CHUNK = 10 * 1024 * 1024        # must be a multiple of 320 KiB\n\nclass GraphClient(GraphClient):\n    def upload_large(self, drive_id, folder, filename, data):\n        \"\"\"Chunked, resumable upload for a big workbook.\"\"\"\n        path = f\"{folder.strip('\u002F')}\u002F{filename}\"\n        create = self.session.post(\n            f\"{GRAPH}\u002Fdrives\u002F{drive_id}\u002Froot:\u002F{path}:\u002FcreateUploadSession\",\n            headers=self.headers(**{\"Content-Type\": \"application\u002Fjson\"}),\n            json={\"item\": {\"@microsoft.graph.conflictBehavior\": \"replace\"}},\n            timeout=30,\n        )\n        create.raise_for_status()\n        upload_url = create.json()[\"uploadUrl\"]\n\n        total = len(data)\n        start = 0\n        while start \u003C total:\n            end = min(start + CHUNK, total) - 1\n            chunk = data[start:end + 1]\n\n            r = self.session.put(\n                upload_url,\n                headers={\n                    \"Content-Length\": str(len(chunk)),\n                    \"Content-Range\": f\"bytes {start}-{end}\u002F{total}\",\n                },\n                data=chunk,\n                timeout=300,\n            )\n            if r.status_code in (200, 201):\n                return r.json()             # finished\n            r.raise_for_status()            # 202 means keep going\n\n            # Graph reports the next byte it wants; trust it over our counter.\n            expected = r.json().get(\"nextExpectedRanges\")\n            start = int(expected[0].split(\"-\")[0]) if expected else end + 1\n\n        raise RuntimeError(\"upload session ended without a completed item\")\n",[192,1818,1819,1842,1846,1858,1867,1872,1902,1913,1947,1970,1996,2006,2010,2015,2030,2034,2047,2057,2070,2096,2113,2117,2127,2132,2141,2159,2200,2205,2215,2227,2232,2253,2263,2271,2275,2280,2295,2340,2344],{"__ignoreMap":190},[195,1820,1821,1824,1826,1829,1832,1835,1837,1839],{"class":78,"line":197},[195,1822,1823],{"class":398},"CHUNK",[195,1825,402],{"class":290},[195,1827,1828],{"class":398}," 10",[195,1830,1831],{"class":290}," *",[195,1833,1834],{"class":398}," 1024",[195,1836,1831],{"class":290},[195,1838,1834],{"class":398},[195,1840,1841],{"class":920},"        # must be a multiple of 320 KiB\n",[195,1843,1844],{"class":78,"line":304},[195,1845,393],{"emptyLinePlaceholder":392},[195,1847,1848,1850,1852,1854,1856],{"class":78,"line":316},[195,1849,423],{"class":290},[195,1851,426],{"class":200},[195,1853,910],{"class":294},[195,1855,914],{"class":913},[195,1857,1048],{"class":294},[195,1859,1860,1862,1865],{"class":78,"line":328},[195,1861,435],{"class":290},[195,1863,1864],{"class":547}," upload_large",[195,1866,1357],{"class":294},[195,1868,1869],{"class":78,"line":341},[195,1870,1871],{"class":204},"        \"\"\"Chunked, resumable upload for a big workbook.\"\"\"\n",[195,1873,1874,1876,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900],{"class":78,"line":415},[195,1875,1367],{"class":294},[195,1877,298],{"class":290},[195,1879,946],{"class":290},[195,1881,853],{"class":204},[195,1883,842],{"class":408},[195,1885,1378],{"class":294},[195,1887,1381],{"class":204},[195,1889,1384],{"class":294},[195,1891,850],{"class":408},[195,1893,1389],{"class":204},[195,1895,842],{"class":408},[195,1897,1394],{"class":294},[195,1899,850],{"class":408},[195,1901,977],{"class":204},[195,1903,1904,1907,1909,1911],{"class":78,"line":420},[195,1905,1906],{"class":294},"        create ",[195,1908,298],{"class":290},[195,1910,565],{"class":398},[195,1912,617],{"class":294},[195,1914,1915,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1945],{"class":78,"line":432},[195,1916,1917],{"class":290},"            f",[195,1919,853],{"class":204},[195,1921,842],{"class":408},[195,1923,892],{"class":398},[195,1925,850],{"class":408},[195,1927,1417],{"class":204},[195,1929,842],{"class":408},[195,1931,1422],{"class":294},[195,1933,850],{"class":408},[195,1935,1427],{"class":204},[195,1937,842],{"class":408},[195,1939,1432],{"class":294},[195,1941,850],{"class":408},[195,1943,1944],{"class":204},":\u002FcreateUploadSession\"",[195,1946,690],{"class":294},[195,1948,1949,1951,1953,1955,1957,1959,1961,1963,1965,1968],{"class":78,"line":444},[195,1950,1462],{"class":629},[195,1952,298],{"class":290},[195,1954,635],{"class":398},[195,1956,1469],{"class":294},[195,1958,817],{"class":290},[195,1960,842],{"class":294},[195,1962,1476],{"class":204},[195,1964,658],{"class":294},[195,1966,1967],{"class":204},"\"application\u002Fjson\"",[195,1969,1483],{"class":294},[195,1971,1972,1975,1977,1979,1982,1985,1988,1990,1993],{"class":78,"line":464},[195,1973,1974],{"class":629},"            json",[195,1976,298],{"class":290},[195,1978,842],{"class":294},[195,1980,1981],{"class":204},"\"item\"",[195,1983,1984],{"class":294},": {",[195,1986,1987],{"class":204},"\"@microsoft.graph.conflictBehavior\"",[195,1989,658],{"class":294},[195,1991,1992],{"class":204},"\"replace\"",[195,1994,1995],{"class":294},"}},\n",[195,1997,1998,2000,2002,2004],{"class":78,"line":481},[195,1999,715],{"class":629},[195,2001,298],{"class":290},[195,2003,720],{"class":398},[195,2005,690],{"class":294},[195,2007,2008],{"class":78,"line":498},[195,2009,728],{"class":294},[195,2011,2012],{"class":78,"line":511},[195,2013,2014],{"class":294},"        create.raise_for_status()\n",[195,2016,2017,2020,2022,2025,2028],{"class":78,"line":524},[195,2018,2019],{"class":294},"        upload_url ",[195,2021,298],{"class":290},[195,2023,2024],{"class":294}," create.json()[",[195,2026,2027],{"class":204},"\"uploadUrl\"",[195,2029,461],{"class":294},[195,2031,2032],{"class":78,"line":537},[195,2033,393],{"emptyLinePlaceholder":392},[195,2035,2036,2039,2041,2044],{"class":78,"line":542},[195,2037,2038],{"class":294},"        total ",[195,2040,298],{"class":290},[195,2042,2043],{"class":398}," len",[195,2045,2046],{"class":294},"(data)\n",[195,2048,2049,2052,2054],{"class":78,"line":553},[195,2050,2051],{"class":294},"        start ",[195,2053,298],{"class":290},[195,2055,2056],{"class":398}," 0\n",[195,2058,2059,2062,2065,2067],{"class":78,"line":559},[195,2060,2061],{"class":290},"        while",[195,2063,2064],{"class":294}," start ",[195,2066,576],{"class":290},[195,2068,2069],{"class":294}," total:\n",[195,2071,2072,2075,2077,2080,2083,2085,2088,2091,2093],{"class":78,"line":591},[195,2073,2074],{"class":294},"            end ",[195,2076,298],{"class":290},[195,2078,2079],{"class":398}," min",[195,2081,2082],{"class":294},"(start ",[195,2084,781],{"class":290},[195,2086,2087],{"class":398}," CHUNK",[195,2089,2090],{"class":294},", total) ",[195,2092,583],{"class":290},[195,2094,2095],{"class":398}," 1\n",[195,2097,2098,2101,2103,2106,2108,2111],{"class":78,"line":602},[195,2099,2100],{"class":294},"            chunk ",[195,2102,298],{"class":290},[195,2104,2105],{"class":294}," data[start:end ",[195,2107,781],{"class":290},[195,2109,2110],{"class":398}," 1",[195,2112,461],{"class":294},[195,2114,2115],{"class":78,"line":607},[195,2116,393],{"emptyLinePlaceholder":392},[195,2118,2119,2121,2123,2125],{"class":78,"line":620},[195,2120,1073],{"class":294},[195,2122,298],{"class":290},[195,2124,565],{"class":398},[195,2126,1452],{"class":294},[195,2128,2129],{"class":78,"line":641},[195,2130,2131],{"class":294},"                upload_url,\n",[195,2133,2134,2137,2139],{"class":78,"line":652},[195,2135,2136],{"class":629},"                headers",[195,2138,298],{"class":290},[195,2140,649],{"class":294},[195,2142,2143,2146,2148,2151,2153,2156],{"class":78,"line":666},[195,2144,2145],{"class":204},"                    \"Content-Length\"",[195,2147,658],{"class":294},[195,2149,2150],{"class":398},"str",[195,2152,910],{"class":294},[195,2154,2155],{"class":398},"len",[195,2157,2158],{"class":294},"(chunk)),\n",[195,2160,2161,2164,2166,2168,2171,2173,2176,2178,2180,2182,2185,2187,2189,2191,2194,2196,2198],{"class":78,"line":679},[195,2162,2163],{"class":204},"                    \"Content-Range\"",[195,2165,658],{"class":294},[195,2167,836],{"class":290},[195,2169,2170],{"class":204},"\"bytes ",[195,2172,842],{"class":408},[195,2174,2175],{"class":294},"start",[195,2177,850],{"class":408},[195,2179,583],{"class":204},[195,2181,842],{"class":408},[195,2183,2184],{"class":294},"end",[195,2186,850],{"class":408},[195,2188,1389],{"class":204},[195,2190,842],{"class":408},[195,2192,2193],{"class":294},"total",[195,2195,850],{"class":408},[195,2197,853],{"class":204},[195,2199,690],{"class":294},[195,2201,2202],{"class":78,"line":693},[195,2203,2204],{"class":294},"                },\n",[195,2206,2207,2210,2212],{"class":78,"line":706},[195,2208,2209],{"class":629},"                data",[195,2211,298],{"class":290},[195,2213,2214],{"class":294},"chunk,\n",[195,2216,2217,2220,2222,2225],{"class":78,"line":712},[195,2218,2219],{"class":629},"                timeout",[195,2221,298],{"class":290},[195,2223,2224],{"class":398},"300",[195,2226,690],{"class":294},[195,2228,2229],{"class":78,"line":725},[195,2230,2231],{"class":294},"            )\n",[195,2233,2234,2236,2239,2241,2244,2246,2248,2251],{"class":78,"line":731},[195,2235,1222],{"class":290},[195,2237,2238],{"class":294}," r.status_code ",[195,2240,1209],{"class":290},[195,2242,2243],{"class":294}," (",[195,2245,171],{"class":398},[195,2247,856],{"class":294},[195,2249,2250],{"class":398},"201",[195,2252,1048],{"class":294},[195,2254,2255,2257,2260],{"class":78,"line":737},[195,2256,1242],{"class":290},[195,2258,2259],{"class":294}," r.json()             ",[195,2261,2262],{"class":920},"# finished\n",[195,2264,2265,2268],{"class":78,"line":748},[195,2266,2267],{"class":294},"            r.raise_for_status()            ",[195,2269,2270],{"class":920},"# 202 means keep going\n",[195,2272,2273],{"class":78,"line":753},[195,2274,393],{"emptyLinePlaceholder":392},[195,2276,2277],{"class":78,"line":770},[195,2278,2279],{"class":920},"            # Graph reports the next byte it wants; trust it over our counter.\n",[195,2281,2282,2285,2287,2290,2293],{"class":78,"line":791},[195,2283,2284],{"class":294},"            expected ",[195,2286,298],{"class":290},[195,2288,2289],{"class":294}," r.json().get(",[195,2291,2292],{"class":204},"\"nextExpectedRanges\"",[195,2294,1009],{"class":294},[195,2296,2297,2300,2302,2305,2308,2310,2313,2316,2319,2321,2324,2327,2330,2333,2336,2338],{"class":78,"line":801},[195,2298,2299],{"class":294},"            start ",[195,2301,298],{"class":290},[195,2303,2304],{"class":398}," int",[195,2306,2307],{"class":294},"(expected[",[195,2309,42],{"class":398},[195,2311,2312],{"class":294},"].split(",[195,2314,2315],{"class":204},"\"-\"",[195,2317,2318],{"class":294},")[",[195,2320,42],{"class":398},[195,2322,2323],{"class":294},"]) ",[195,2325,2326],{"class":290},"if",[195,2328,2329],{"class":294}," expected ",[195,2331,2332],{"class":290},"else",[195,2334,2335],{"class":294}," end ",[195,2337,781],{"class":290},[195,2339,2095],{"class":398},[195,2341,2342],{"class":78,"line":806},[195,2343,393],{"emptyLinePlaceholder":392},[195,2345,2346,2348,2351,2353,2356],{"class":78,"line":823},[195,2347,1253],{"class":290},[195,2349,2350],{"class":398}," RuntimeError",[195,2352,910],{"class":294},[195,2354,2355],{"class":204},"\"upload session ended without a completed item\"",[195,2357,1009],{"class":294},[10,2359,2360,2361,2364,2365,2368,2369,2372,2373,2376],{},"Two rules are non-negotiable. Chunk size ",[219,2362,2363],{},"must be a multiple of 320 KiB"," — Graph rejects anything else outright. And the upload URL from ",[192,2366,2367],{},"createUploadSession"," is already authorised, so do ",[219,2370,2371],{},"not"," send the ",[192,2374,2375],{},"Authorization"," header with the chunks; including it causes failures that look like permission problems.",[10,2378,2379,2380,2383],{},"Trusting ",[192,2381,2382],{},"nextExpectedRanges"," rather than your own counter is what makes the loop resumable. If a chunk partially landed, Graph tells you exactly where to pick up.",[180,2385,2387],{"id":2386},"step-5-get-a-link-people-can-use","Step 5 — Get a link people can use",[10,2389,2390,2391,2394],{},"The ",[192,2392,2393],{},"webUrl"," on the returned item is the document's address, which works for anyone who already has site access. To hand out an explicit link, ask Graph to create one:",[185,2396,2398],{"className":361,"code":2397,"language":363,"meta":190,"style":190},"class GraphClient(GraphClient):\n    def share_link(self, drive_id, item_id, link_type=\"view\",\n                   scope=\"organization\"):\n        \"\"\"Create a sharing link for an uploaded item.\"\"\"\n        r = self.session.post(\n            f\"{GRAPH}\u002Fdrives\u002F{drive_id}\u002Fitems\u002F{item_id}\u002FcreateLink\",\n            headers=self.headers(**{\"Content-Type\": \"application\u002Fjson\"}),\n            json={\"type\": link_type, \"scope\": scope},\n            timeout=30,\n        )\n        r.raise_for_status()\n        return r.json()[\"link\"][\"webUrl\"]\n\nurl = graph.share_link(drive, item[\"id\"])\nprint(url)\n",[192,2399,2400,2412,2429,2441,2446,2456,2491,2513,2533,2543,2547,2551,2567,2571,2585],{"__ignoreMap":190},[195,2401,2402,2404,2406,2408,2410],{"class":78,"line":197},[195,2403,423],{"class":290},[195,2405,426],{"class":200},[195,2407,910],{"class":294},[195,2409,914],{"class":913},[195,2411,1048],{"class":294},[195,2413,2414,2416,2419,2422,2424,2427],{"class":78,"line":304},[195,2415,435],{"class":290},[195,2417,2418],{"class":547}," share_link",[195,2420,2421],{"class":294},"(self, drive_id, item_id, link_type",[195,2423,298],{"class":290},[195,2425,2426],{"class":204},"\"view\"",[195,2428,690],{"class":294},[195,2430,2431,2434,2436,2439],{"class":78,"line":316},[195,2432,2433],{"class":294},"                   scope",[195,2435,298],{"class":290},[195,2437,2438],{"class":204},"\"organization\"",[195,2440,1048],{"class":294},[195,2442,2443],{"class":78,"line":328},[195,2444,2445],{"class":204},"        \"\"\"Create a sharing link for an uploaded item.\"\"\"\n",[195,2447,2448,2450,2452,2454],{"class":78,"line":341},[195,2449,982],{"class":294},[195,2451,298],{"class":290},[195,2453,565],{"class":398},[195,2455,617],{"class":294},[195,2457,2458,2460,2462,2464,2466,2468,2470,2472,2474,2476,2479,2481,2484,2486,2489],{"class":78,"line":415},[195,2459,1917],{"class":290},[195,2461,853],{"class":204},[195,2463,842],{"class":408},[195,2465,892],{"class":398},[195,2467,850],{"class":408},[195,2469,1417],{"class":204},[195,2471,842],{"class":408},[195,2473,1422],{"class":294},[195,2475,850],{"class":408},[195,2477,2478],{"class":204},"\u002Fitems\u002F",[195,2480,842],{"class":408},[195,2482,2483],{"class":294},"item_id",[195,2485,850],{"class":408},[195,2487,2488],{"class":204},"\u002FcreateLink\"",[195,2490,690],{"class":294},[195,2492,2493,2495,2497,2499,2501,2503,2505,2507,2509,2511],{"class":78,"line":420},[195,2494,1462],{"class":629},[195,2496,298],{"class":290},[195,2498,635],{"class":398},[195,2500,1469],{"class":294},[195,2502,817],{"class":290},[195,2504,842],{"class":294},[195,2506,1476],{"class":204},[195,2508,658],{"class":294},[195,2510,1967],{"class":204},[195,2512,1483],{"class":294},[195,2514,2515,2517,2519,2521,2524,2527,2530],{"class":78,"line":432},[195,2516,1974],{"class":629},[195,2518,298],{"class":290},[195,2520,842],{"class":294},[195,2522,2523],{"class":204},"\"type\"",[195,2525,2526],{"class":294},": link_type, ",[195,2528,2529],{"class":204},"\"scope\"",[195,2531,2532],{"class":294},": scope},\n",[195,2534,2535,2537,2539,2541],{"class":78,"line":444},[195,2536,715],{"class":629},[195,2538,298],{"class":290},[195,2540,720],{"class":398},[195,2542,690],{"class":294},[195,2544,2545],{"class":78,"line":464},[195,2546,728],{"class":294},[195,2548,2549],{"class":78,"line":481},[195,2550,1014],{"class":294},[195,2552,2553,2555,2557,2560,2563,2565],{"class":78,"line":498},[195,2554,794],{"class":290},[195,2556,1021],{"class":294},[195,2558,2559],{"class":204},"\"link\"",[195,2561,2562],{"class":294},"][",[195,2564,1712],{"class":204},[195,2566,461],{"class":294},[195,2568,2569],{"class":78,"line":511},[195,2570,393],{"emptyLinePlaceholder":392},[195,2572,2573,2576,2578,2581,2583],{"class":78,"line":524},[195,2574,2575],{"class":294},"url ",[195,2577,298],{"class":290},[195,2579,2580],{"class":294}," graph.share_link(drive, item[",[195,2582,1024],{"class":204},[195,2584,1556],{"class":294},[195,2586,2587,2589],{"class":78,"line":537},[195,2588,1706],{"class":398},[195,2590,2591],{"class":294},"(url)\n",[10,2593,2594,2595,2598,2599,2602],{},"Use ",[192,2596,2597],{},"scope=\"organization\""," so the link works for signed-in colleagues and nobody else. ",[192,2600,2601],{},"scope=\"anonymous\""," produces a link anyone can open, which most tenants disable by policy — and should, for a report.",[180,2604,2606],{"id":2605},"common-pitfalls-and-fixes","Common pitfalls and fixes",[224,2608,2609,2622],{},[227,2610,2611],{},[230,2612,2613,2616,2619],{},[233,2614,2615],{},"Symptom",[233,2617,2618],{},"Cause",[233,2620,2621],{},"Fix",[243,2623,2624,2639,2656,2670,2683,2701,2718,2732],{},[230,2625,2626,2631,2634],{},[248,2627,2628,2630],{},[192,2629,279],{}," with a valid token",[248,2632,2633],{},"Admin consent or site grant missing",[248,2635,2636,2637,18],{},"Consent the application permission; grant the site for ",[192,2638,252],{},[230,2640,2641,2647,2650],{},[248,2642,2643,2646],{},[192,2644,2645],{},"401"," after an hour",[248,2648,2649],{},"Cached token expired",[248,2651,2652,2653,18],{},"Refresh a minute before ",[192,2654,2655],{},"expires_in",[230,2657,2658,2664,2667],{},[248,2659,2660,2663],{},[192,2661,2662],{},"400 invalidRequest"," on a chunk",[248,2665,2666],{},"Chunk size not a multiple of 320 KiB",[248,2668,2669],{},"Use 320 KiB multiples, e.g. 10 MB.",[230,2671,2672,2675,2680],{},[248,2673,2674],{},"Chunk uploads fail oddly",[248,2676,2677,2679],{},[192,2678,2375],{}," sent with chunks",[248,2681,2682],{},"The session URL is pre-authorised; omit the header.",[230,2684,2685,2691,2694],{},[248,2686,2687,2690],{},[192,2688,2689],{},"404"," resolving the site",[248,2692,2693],{},"Wrong hostname\u002Fpath separator",[248,2695,2696,2697,2700],{},"The format is ",[192,2698,2699],{},"{hostname}:{\u002Fsites\u002Fname}"," with the colon.",[230,2702,2703,2708,2711],{},[248,2704,2705],{},[192,2706,2707],{},"429 Too Many Requests",[248,2709,2710],{},"Throttled",[248,2712,2713,2714,2717],{},"Honour ",[192,2715,2716],{},"Retry-After","; cache tokens and IDs.",[230,2719,2720,2723,2726],{},[248,2721,2722],{},"File uploads but opens as binary",[248,2724,2725],{},"Content type not set on PUT",[248,2727,2728,2729,18],{},"Send the spreadsheetml ",[192,2730,2731],{},"Content-Type",[230,2733,2734,2737,2742],{},[248,2735,2736],{},"Session upload restarts from zero",[248,2738,2739,2740],{},"Ignoring ",[192,2741,2382],{},[248,2743,2744],{},"Resume from the range Graph reports.",[180,2746,2748],{"id":2747},"performance-and-scale-notes","Performance and scale notes",[20,2750,30,2756,30,2759,30,2762,30,2765,30,2769,30,2773,30,2778,30,2783,30,2788,30,2791,30,2795,30,2798,30,2802,30,2805,30,2810,30,2813,30,2818,30,2820,30,2822,30,2827],{"viewBox":2751,"role":23,"ariaLabel":2752,"ariaLabelledBy":2753,"xmlns":28,"style":29},"0 0 800 224","Call volume per report with and without caching: without caching each report costs a token request plus two lookups plus the upload, while caching reduces it to the upload alone.",[2754,2755],"cache-t","cache-d",[32,2757,2758],{"id":2754},"Graph calls per report, before and after caching",[36,2760,2761],{"id":2755},"Two bars showing calls made per published report. Without caching, each report costs four calls: a token request, a site lookup, a drive lookup and the upload itself. With the token and the resolved identifiers cached, only the upload remains, cutting the request volume to a quarter and making tenant throttling far less likely on a fan-out job.",[40,2763],{"x":42,"y":42,"width":43,"height":2764,"fill":45},"224",[57,2766,2768],{"x":170,"y":2767,"style":1744},"28","Graph calls per published report",[40,2770],{"x":48,"y":2771,"width":162,"height":720,"rx":2772,"fill":146,"stroke":147},"56","6",[57,2774,2777],{"x":2775,"y":2776,"style":155},"74","76","no caching",[40,2779],{"x":2780,"y":2771,"width":2781,"height":720,"rx":2782,"fill":166,"stroke":167,"style":55},"148","144","5",[57,2784,2787],{"x":2785,"y":2776,"style":2786},"220","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","token",[40,2789],{"x":2790,"y":2771,"width":2781,"height":720,"rx":2782,"fill":166,"stroke":167,"style":55},"296",[57,2792,2794],{"x":2793,"y":2776,"style":2786},"368","site lookup",[40,2796],{"x":2797,"y":2771,"width":2781,"height":720,"rx":2782,"fill":166,"stroke":167,"style":55},"444",[57,2799,2801],{"x":2800,"y":2776,"style":2786},"516","drive lookup",[40,2803],{"x":2804,"y":2771,"width":2781,"height":720,"rx":2782,"fill":53,"stroke":54,"style":55},"592",[57,2806,2809],{"x":2807,"y":2776,"style":2808},"664","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","upload",[40,2811],{"x":48,"y":2812,"width":162,"height":720,"rx":2772,"fill":146,"stroke":147},"126",[57,2814,2817],{"x":2775,"y":2815,"style":2816},"146","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","IDs cached",[40,2819],{"x":2780,"y":2812,"width":2781,"height":720,"rx":2782,"fill":53,"stroke":54,"style":55},[57,2821,2809],{"x":2785,"y":2815,"style":2808},[57,2823,2826],{"x":2824,"y":2815,"style":2825},"316","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157)","one quarter of the request volume",[57,2828,2831],{"x":170,"y":2829,"style":2830},"196","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","on a forty-report fan-out that is 160 calls versus 40 — the difference between throttled and not",[10,2833,2834,2835,2837],{},"Graph throttles per app and per tenant, and a report fan-out is exactly the shape that triggers it. Honouring ",[192,2836,2716],{}," is not optional — it is the documented contract, and ignoring it extends the throttling window:",[185,2839,2841],{"className":361,"code":2840,"language":363,"meta":190,"style":190},"import time\n\ndef request_with_backoff(session, method, url, attempts=5, **kwargs):\n    for attempt in range(1, attempts + 1):\n        response = session.request(method, url, **kwargs)\n        if response.status_code == 429 and attempt \u003C attempts:\n            wait = int(response.headers.get(\"Retry-After\", 2 ** attempt))\n            print(f\"throttled; sleeping {wait}s\")\n            time.sleep(wait)\n            continue\n        return response\n    return response\n",[192,2842,2843,2849,2853,2875,2902,2916,2938,2964,2988,2993,2998,3005],{"__ignoreMap":190},[195,2844,2845,2847],{"class":78,"line":197},[195,2846,370],{"class":290},[195,2848,380],{"class":294},[195,2850,2851],{"class":78,"line":304},[195,2852,393],{"emptyLinePlaceholder":392},[195,2854,2855,2858,2861,2864,2866,2868,2870,2872],{"class":78,"line":316},[195,2856,2857],{"class":290},"def",[195,2859,2860],{"class":547}," request_with_backoff",[195,2862,2863],{"class":294},"(session, method, url, attempts",[195,2865,298],{"class":290},[195,2867,2782],{"class":398},[195,2869,856],{"class":294},[195,2871,817],{"class":290},[195,2873,2874],{"class":294},"kwargs):\n",[195,2876,2877,2880,2883,2885,2888,2890,2893,2896,2898,2900],{"class":78,"line":328},[195,2878,2879],{"class":290},"    for",[195,2881,2882],{"class":294}," attempt ",[195,2884,1209],{"class":290},[195,2886,2887],{"class":398}," range",[195,2889,910],{"class":294},[195,2891,2892],{"class":398},"1",[195,2894,2895],{"class":294},", attempts ",[195,2897,781],{"class":290},[195,2899,2110],{"class":398},[195,2901,1048],{"class":294},[195,2903,2904,2906,2908,2911,2913],{"class":78,"line":341},[195,2905,610],{"class":294},[195,2907,298],{"class":290},[195,2909,2910],{"class":294}," session.request(method, url, ",[195,2912,817],{"class":290},[195,2914,2915],{"class":294},"kwargs)\n",[195,2917,2918,2920,2923,2925,2928,2931,2933,2935],{"class":78,"line":415},[195,2919,562],{"class":290},[195,2921,2922],{"class":294}," response.status_code ",[195,2924,1234],{"class":290},[195,2926,2927],{"class":398}," 429",[195,2929,2930],{"class":290}," and",[195,2932,2882],{"class":294},[195,2934,576],{"class":290},[195,2936,2937],{"class":294}," attempts:\n",[195,2939,2940,2943,2945,2947,2950,2953,2955,2958,2961],{"class":78,"line":420},[195,2941,2942],{"class":294},"            wait ",[195,2944,298],{"class":290},[195,2946,2304],{"class":398},[195,2948,2949],{"class":294},"(response.headers.get(",[195,2951,2952],{"class":204},"\"Retry-After\"",[195,2954,856],{"class":294},[195,2956,2957],{"class":398},"2",[195,2959,2960],{"class":290}," **",[195,2962,2963],{"class":294}," attempt))\n",[195,2965,2966,2969,2971,2973,2976,2978,2981,2983,2986],{"class":78,"line":432},[195,2967,2968],{"class":398},"            print",[195,2970,910],{"class":294},[195,2972,836],{"class":290},[195,2974,2975],{"class":204},"\"throttled; sleeping ",[195,2977,842],{"class":408},[195,2979,2980],{"class":294},"wait",[195,2982,850],{"class":408},[195,2984,2985],{"class":204},"s\"",[195,2987,1009],{"class":294},[195,2989,2990],{"class":78,"line":444},[195,2991,2992],{"class":294},"            time.sleep(wait)\n",[195,2994,2995],{"class":78,"line":464},[195,2996,2997],{"class":290},"            continue\n",[195,2999,3000,3002],{"class":78,"line":481},[195,3001,794],{"class":290},[195,3003,3004],{"class":294}," response\n",[195,3006,3007,3010],{"class":78,"line":498},[195,3008,3009],{"class":290},"    return",[195,3011,3004],{"class":294},[10,3013,3014,3015,3018,3019,3022,3023,3026],{},"Three habits keep a multi-report job inside the limits. ",[219,3016,3017],{},"Cache the token"," — one per hour, not one per file. ",[219,3020,3021],{},"Cache the site and drive IDs"," in configuration rather than resolving them per upload; that removes two calls from every report. And ",[219,3024,3025],{},"keep concurrency modest",": four to six parallel uploads is usually the sweet spot, beyond which throttling costs more than the parallelism gains.",[185,3028,3030],{"className":361,"code":3029,"language":363,"meta":190,"style":190},"from concurrent.futures import ThreadPoolExecutor\n\ndef publish_region(region):\n    buffer = build_report(region)                 # returns bytes\n    return graph.upload_small(drive, \"Reports\u002F2026\",\n                              f\"{region}-2026-08.xlsx\", buffer)\n\nwith ThreadPoolExecutor(max_workers=4) as pool:\n    for item in pool.map(publish_region, [\"north\", \"south\", \"west\", \"east\"]):\n        print(item[\"webUrl\"])\n",[192,3031,3032,3045,3049,3059,3072,3082,3102,3106,3128,3161],{"__ignoreMap":190},[195,3033,3034,3037,3040,3042],{"class":78,"line":197},[195,3035,3036],{"class":290},"from",[195,3038,3039],{"class":294}," concurrent.futures ",[195,3041,370],{"class":290},[195,3043,3044],{"class":294}," ThreadPoolExecutor\n",[195,3046,3047],{"class":78,"line":304},[195,3048,393],{"emptyLinePlaceholder":392},[195,3050,3051,3053,3056],{"class":78,"line":316},[195,3052,2857],{"class":290},[195,3054,3055],{"class":547}," publish_region",[195,3057,3058],{"class":294},"(region):\n",[195,3060,3061,3064,3066,3069],{"class":78,"line":328},[195,3062,3063],{"class":294},"    buffer ",[195,3065,298],{"class":290},[195,3067,3068],{"class":294}," build_report(region)                 ",[195,3070,3071],{"class":920},"# returns bytes\n",[195,3073,3074,3076,3078,3080],{"class":78,"line":341},[195,3075,3009],{"class":290},[195,3077,1686],{"class":294},[195,3079,1689],{"class":204},[195,3081,690],{"class":294},[195,3083,3084,3087,3089,3091,3094,3096,3099],{"class":78,"line":415},[195,3085,3086],{"class":290},"                              f",[195,3088,853],{"class":204},[195,3090,842],{"class":408},[195,3092,3093],{"class":294},"region",[195,3095,850],{"class":408},[195,3097,3098],{"class":204},"-2026-08.xlsx\"",[195,3100,3101],{"class":294},", buffer)\n",[195,3103,3104],{"class":78,"line":420},[195,3105,393],{"emptyLinePlaceholder":392},[195,3107,3108,3110,3113,3116,3118,3121,3123,3125],{"class":78,"line":432},[195,3109,1628],{"class":290},[195,3111,3112],{"class":294}," ThreadPoolExecutor(",[195,3114,3115],{"class":629},"max_workers",[195,3117,298],{"class":290},[195,3119,3120],{"class":398},"4",[195,3122,1642],{"class":294},[195,3124,1314],{"class":290},[195,3126,3127],{"class":294}," pool:\n",[195,3129,3130,3132,3135,3137,3140,3143,3145,3148,3150,3153,3155,3158],{"class":78,"line":444},[195,3131,2879],{"class":290},[195,3133,3134],{"class":294}," item ",[195,3136,1209],{"class":290},[195,3138,3139],{"class":294}," pool.map(publish_region, [",[195,3141,3142],{"class":204},"\"north\"",[195,3144,856],{"class":294},[195,3146,3147],{"class":204},"\"south\"",[195,3149,856],{"class":294},[195,3151,3152],{"class":204},"\"west\"",[195,3154,856],{"class":294},[195,3156,3157],{"class":204},"\"east\"",[195,3159,3160],{"class":294},"]):\n",[195,3162,3163,3166,3168,3170],{"class":78,"line":464},[195,3164,3165],{"class":398},"        print",[195,3167,1709],{"class":294},[195,3169,1712],{"class":204},[195,3171,1556],{"class":294},[10,3173,3174,3175,3177,3178,3181],{},"Threads share the ",[192,3176,914],{}," and therefore the cached token, which is the point — each worker reuses one authentication rather than requesting its own. ",[192,3179,3180],{},"requests.Session"," is thread-safe for this pattern, and reusing it also reuses the underlying connection pool.",[10,3183,3184,3185,3188,3189,3193],{},"For memory, the chunked path holds only one chunk at a time in the request body, but the example above keeps the whole file in ",[192,3186,3187],{},"data",". On genuinely large reports, stream from disk instead by slicing a file object per chunk, and build the workbook itself with the streaming approach in ",[14,3190,3192],{"href":3191},"\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"," so peak memory never holds the full grid.",[180,3195,3197],{"id":3196},"conclusion","Conclusion",[10,3199,3200,3201,3203,3204,3206],{},"Publishing to SharePoint from Python is four Graph calls: token, site, drive, upload. Use a client-credentials app registration so the job does not depend on a person's account, prefer ",[192,3202,252],{}," and remember the per-site grant that goes with it, and cache both the token and the resolved IDs. A direct PUT covers small reports and replaces the previous version while keeping history; anything larger belongs in an upload session with 320 KiB-multiple chunks and no ",[192,3205,2375],{}," header. Then hand out an organisation-scoped sharing link rather than an attachment.",[180,3208,3210],{"id":3209},"frequently-asked-questions","Frequently asked questions",[10,3212,3213,3216,3217,3219,3220,3222,3223,3225],{},[219,3214,3215],{},"Which permissions does the app registration need?","\nThe application permission ",[192,3218,265],{},", or ",[192,3221,252],{}," with the site granted explicitly. ",[192,3224,252],{}," is the tighter choice because it limits the app to the one site holding the reports rather than every site in the tenant.",[10,3227,3228,3234,3235,3237],{},[219,3229,3230,3231,3233],{},"Why do I get ",[192,3232,279],{}," even though the token was issued?","\nA token proves authentication, not authorisation. Application permissions require admin consent in Entra ID, and with ",[192,3236,252],{}," the app must also be granted access to that specific site. Check both.",[10,3239,3240,3243],{},[219,3241,3242],{},"What is the file size limit for a simple upload?","\nGraph accepts a direct PUT to the content endpoint for files up to about 250 MB, but the practical limit is much lower over an unreliable link because nothing can resume. Use an upload session for anything above a few megabytes.",[10,3245,3246,3249,3250,3253,3254,3257],{},[219,3247,3248],{},"How do I overwrite an existing report each month?","\nA PUT to the item content endpoint replaces the file by default and keeps the version history. For upload sessions set ",[192,3251,3252],{},"conflictBehavior"," to ",[192,3255,3256],{},"replace"," in the session request body.",[10,3259,3260,3263],{},[219,3261,3262],{},"Can I write directly into a Teams channel's files?","\nYes — a Teams channel's Files tab is a folder in the team's SharePoint document library, so the same drive endpoints work once you resolve the group's drive.",[180,3265,3267],{"id":3266},"related","Related",[3269,3270,3271,3278,3285,3292,3299],"ul",{},[3272,3273,3274,3275,3277],"li",{},"Up to the parent: ",[14,3276,17],{"href":16}," — naming, atomicity and retry patterns across every destination.",[3272,3279,3280,3284],{},[14,3281,3283],{"href":3282},"\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 object-storage equivalent.",[3272,3286,3287,3291],{},[14,3288,3290],{"href":3289},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fsave-excel-reports-to-google-drive-with-python\u002F","Save Excel Reports to Google Drive with Python"," — the Google Workspace path.",[3272,3293,3294,3298],{},[14,3295,3297],{"href":3296},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-api-data-into-excel-with-python-requests\u002F","Fetch API Data into Excel with Python requests"," — the same HTTP discipline on the way in.",[3272,3300,3301,3305],{},[14,3302,3304],{"href":3303},"\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 around throttling and transient failures.",[3307,3308,3309],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":190,"searchDepth":304,"depth":304,"links":3311},[3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322],{"id":182,"depth":304,"text":183},{"id":354,"depth":304,"text":355},{"id":878,"depth":304,"text":879},{"id":1288,"depth":304,"text":1289},{"id":1720,"depth":304,"text":1721},{"id":2386,"depth":304,"text":2387},{"id":2605,"depth":304,"text":2606},{"id":2747,"depth":304,"text":2748},{"id":3196,"depth":304,"text":3197},{"id":3209,"depth":304,"text":3210},{"id":3266,"depth":304,"text":3267},"2026-08-15","Publish a generated .xlsx to SharePoint or OneDrive from Python using Microsoft Graph — app-only tokens, resolving the drive, simple and chunked uploads, and sharing links.","md",[3327,3329,3332,3334,3336],{"q":3215,"a":3328},"The application permission Files.ReadWrite.All, or Sites.Selected with the site granted explicitly. Sites.Selected is the tighter choice because it limits the app to the one site holding the reports rather than every site in the tenant.",{"q":3330,"a":3331},"Why do I get 403 even though the token was issued?","A token proves authentication, not authorisation. Application permissions require admin consent in Entra ID, and with Sites.Selected the app must also be granted access to that specific site. Check both.",{"q":3242,"a":3333},"Graph accepts a direct PUT to the content endpoint for files up to about 250 MB, but the practical limit is much lower over an unreliable link because nothing can resume. Use an upload session for anything above a few megabytes.",{"q":3248,"a":3335},"A PUT to the item content endpoint replaces the file by default and keeps the version history. For upload sessions set conflictBehavior to replace in the session request body.",{"q":3262,"a":3337},"Yes — a Teams channel's Files tab is a folder in the team's SharePoint document library, so the same drive endpoints work once you resolve the group's drive.",{},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python",{"title":3341,"description":3342},"Upload an Excel File to SharePoint with Python (Graph)","Publish Excel reports to SharePoint from a scheduled job: client-credentials tokens, site and drive lookup, small and large file uploads, conflict behaviour and sharing links.","upload-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","how-to","yE62A2EZ9fQPMPXPE1tC-DbszTYO8iLZWHL5RbGOfXk",[3348,3351],{"title":3283,"path":3349,"stem":3350,"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":3352,"path":3353,"stem":3354,"children":-1},"Write Excel Files to a Network Share from Python","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python\u002Findex",1786800027148]