[{"data":1,"prerenderedAt":3279},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph":3,"surround:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph":3249,"ancestors:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph":3257,"children:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph":3259,"related:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph":3267},{"id":4,"title":5,"body":6,"dateModified":3213,"datePublished":3213,"description":3214,"extension":3215,"faq":3216,"keyword":3229,"kind":3230,"meta":3231,"navigation":260,"path":3239,"seo":3240,"slug":3243,"stem":3244,"topics":3245,"type":3247,"__hash__":3248},"docs\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Findex.md","Automating Excel Online with Microsoft Graph",{"type":7,"value":8,"toc":3196},"minimark",[9,19,152,157,196,204,208,225,573,578,758,762,765,855,858,862,873,1168,1175,1179,1194,1324,1327,1336,1340,1351,1598,1630,1710,1714,1721,1954,1967,1971,1986,2250,2270,2374,2378,2398,2601,2608,2612,2627,2880,2887,2891,2898,2902,3002,3006,3052,3056,3062,3068,3074,3080,3086,3092,3096,3145,3148,3192],[10,11,12,13,18],"p",{},"In a Microsoft 365 organisation the important workbooks are no longer on anyone's C: drive. They sit in\nSharePoint document libraries and OneDrive folders, several people have them open at once, and Power\nAutomate or Power BI may already read from them. A reporting script that expects a local path does not fit\nthat world, and one that downloads, edits and re-uploads files will sooner or later overwrite somebody's\nchanges. Microsoft Graph offers two better routes: file endpoints that move workbook bytes in and out, and a\nworkbook API that edits ranges and tables inside Excel's online service. This section of\n",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002F","Automating Reporting Workflows"," shows when to use each, and how to build\njobs on them that survive a schedule.",[20,21,29,30,29,34,29,38,29,45,29,55,29,60,29,65,29,69,29,72,29,77,29,84,29,89,29,94,29,99,29,103,29,105,29,108,29,111,29,115,29,118,29,121,29,124,29,126,29,129,29,134,29,138,29,142,29,145,29,147],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 282","img",[25,26],"gxl-routes-t","gxl-routes-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;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},"Choosing between the file endpoints and the workbook API",[35,36,37],"desc",{"id":26},"To read a whole workbook into pandas or publish a finished file, use the file endpoints with an app-only token. To change ranges or table rows in a file people may have open, or to use Excel formulas as a calculation engine, use the workbook API with a delegated token.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","282","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"190","20","380","52","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[56,57,59],"text",{"x":49,"y":50,"style":58},"font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","what does the job do with the workbook?",[61,62],"line",{"x1":49,"y1":63,"x2":49,"y2":64,"stroke":53,"style":54},"72","100",[61,66],{"x1":67,"y1":64,"x2":68,"y2":64,"stroke":53,"style":54},"133.3","626.7",[61,70],{"x1":67,"y1":64,"x2":67,"y2":71,"stroke":53,"style":54},"124",[73,74],"polygon",{"points":75,"fill":76},"133.3,127 128.3,118 138.3,118","#5b5cf0",[39,78],{"x":48,"y":79,"width":80,"height":81,"rx":51,"fill":82,"stroke":83,"style":54},"128","226.7","104","#d9f4f1","var(--teal,#0f9488)",[56,85,88],{"x":67,"y":86,"style":87},"150","font-size:11px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","read all \u002F publish file",[56,90,93],{"x":67,"y":91,"style":92},"174","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","file endpoints",[56,95,98],{"x":67,"y":96,"style":97},"196","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","download or upload",[56,100,102],{"x":67,"y":101,"style":97},"216","app-only token ok",[61,104],{"x1":49,"y1":64,"x2":49,"y2":71,"stroke":53,"style":54},[73,106],{"points":107,"fill":76},"380,127 375,118 385,118",[39,109],{"x":110,"y":79,"width":80,"height":81,"rx":51,"fill":52,"stroke":53,"style":54},"266.7",[56,112,114],{"x":49,"y":86,"style":113},"font-size:11px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle","edit ranges or rows",[56,116,117],{"x":49,"y":91,"style":58},"workbook API",[56,119,120],{"x":49,"y":96,"style":97},"in place, co-authoring",[56,122,123],{"x":49,"y":101,"style":97},"delegated token",[61,125],{"x1":68,"y1":64,"x2":68,"y2":71,"stroke":53,"style":54},[73,127],{"points":128,"fill":76},"626.7,127 621.7,118 631.7,118",[39,130],{"x":131,"y":79,"width":80,"height":81,"rx":51,"fill":132,"stroke":133,"style":54},"513.3","#fdefd8","var(--gold,#b4740a)",[56,135,137],{"x":68,"y":86,"style":136},"font-size:11px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","use formulas as a model",[56,139,141],{"x":68,"y":91,"style":140},"font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","sessions + calculate",[56,143,144],{"x":68,"y":96,"style":97},"what-if without saving",[56,146,123],{"x":68,"y":101,"style":97},[56,148,151],{"x":49,"y":149,"style":150},"266","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","many jobs read with the first route and write with the second",[153,154,156],"h2",{"id":155},"what-this-section-covers","What this section covers",[158,159,160,168,175,182,189],"ul",{},[161,162,163,167],"li",{},[14,164,166],{"href":165},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002F","Authenticate to Microsoft Graph with MSAL for Excel Automation"," — delegated tokens with a persistent cache, and app-only tokens where they are allowed.",[161,169,170,174],{},[14,171,173],{"href":172},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-an-excel-file-from-sharepoint-or-onedrive-into-pandas\u002F","Read an Excel File from SharePoint or OneDrive into pandas"," — resolve sites and drives, download into memory, skip unchanged files.",[161,176,177,181],{},[14,178,180],{"href":179},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-and-write-excel-online-ranges-with-microsoft-graph\u002F","Read and Write Excel Online Ranges with Microsoft Graph"," — ranges, used ranges, dates, blanks and formulas.",[161,183,184,188],{},[14,185,187],{"href":186},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph\u002F","Append Rows to an Excel Online Table with Microsoft Graph"," — idempotent, batched feeds into shared tables.",[161,190,191,195],{},[14,192,194],{"href":193},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frecalculate-excel-online-workbooks-with-graph-sessions\u002F","Recalculate Excel Online Workbooks with Graph Sessions"," — sessions, calculation and what-if scenarios.",[10,197,198,199,203],{},"Publishing a finished report to SharePoint is covered separately in\n",[14,200,202],{"href":201},"\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",".",[153,205,207],{"id":206},"the-rule-that-shapes-everything-which-token-for-which-call","The rule that shapes everything: which token for which call",[10,209,210,211,215,216,219,220,224],{},"Graph issues two kinds of access token. An ",[212,213,214],"strong",{},"app-only"," token comes from a client secret or certificate and represents the application itself; it\nis ideal for scheduled jobs because no person is involved. A ",[212,217,218],{},"delegated"," token represents a signed-in user and carries that user's access. The\nfile endpoints — download a workbook's content, upload a new version — accept application permissions. The workbook API endpoints — ranges, tables,\ncalculation, sessions — list application permissions as ",[221,222,223],"em",{},"not supported"," in Microsoft's reference. That single fact decides how most jobs in this\nsection are built.",[226,227,232],"pre",{"className":228,"code":229,"language":230,"meta":231,"style":231},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nimport msal\n\nAUTHORITY = f\"https:\u002F\u002Flogin.microsoftonline.com\u002F{os.environ['GRAPH_TENANT_ID']}\"\n\ndef app_token() -> str:                       # file endpoints: download, upload\n    app = msal.ConfidentialClientApplication(os.environ[\"GRAPH_CLIENT_ID\"], authority=AUTHORITY,\n                                             client_credential=os.environ[\"GRAPH_CLIENT_SECRET\"])\n    return app.acquire_token_for_client(scopes=[\"https:\u002F\u002Fgraph.microsoft.com\u002F.default\"])[\"access_token\"]\n\ndef delegated_token(cache: msal.SerializableTokenCache) -> str:   # workbook API: ranges, tables, calculate\n    app = msal.PublicClientApplication(os.environ[\"GRAPH_CLIENT_ID\"], authority=AUTHORITY, token_cache=cache)\n    accounts = app.get_accounts()\n    result = app.acquire_token_silent([\"Files.ReadWrite\"], account=accounts[0]) if accounts else None\n    if not result:\n        raise RuntimeError(\"no cached sign-in: run the device code sign-in once by hand\")\n    return result[\"access_token\"]\n\nprint(app_token()[:8], delegated_token(msal.SerializableTokenCache())[:8])\n","python","",[233,234,235,247,255,262,298,303,326,355,371,400,405,424,455,466,507,519,537,549,554],"code",{"__ignoreMap":231},[236,237,239,243],"span",{"class":61,"line":238},1,[236,240,242],{"class":241},"s-kum","import",[236,244,246],{"class":245},"skGVy"," os\n",[236,248,250,252],{"class":61,"line":249},2,[236,251,242],{"class":241},[236,253,254],{"class":245}," msal\n",[236,256,258],{"class":61,"line":257},3,[236,259,261],{"emptyLinePlaceholder":260},true,"\n",[236,263,265,269,272,275,279,283,286,289,292,295],{"class":61,"line":264},4,[236,266,268],{"class":267},"sP0c6","AUTHORITY",[236,270,271],{"class":241}," =",[236,273,274],{"class":241}," f",[236,276,278],{"class":277},"srMev","\"https:\u002F\u002Flogin.microsoftonline.com\u002F",[236,280,282],{"class":281},"sSjpA","{",[236,284,285],{"class":245},"os.environ[",[236,287,288],{"class":277},"'GRAPH_TENANT_ID'",[236,290,291],{"class":245},"]",[236,293,294],{"class":281},"}",[236,296,297],{"class":277},"\"\n",[236,299,301],{"class":61,"line":300},5,[236,302,261],{"emptyLinePlaceholder":260},[236,304,306,309,313,316,319,322],{"class":61,"line":305},6,[236,307,308],{"class":241},"def",[236,310,312],{"class":311},"s_Opv"," app_token",[236,314,315],{"class":245},"() -> ",[236,317,318],{"class":267},"str",[236,320,321],{"class":245},":                       ",[236,323,325],{"class":324},"s-wDw","# file endpoints: download, upload\n",[236,327,329,332,335,338,341,344,348,350,352],{"class":61,"line":328},7,[236,330,331],{"class":245},"    app ",[236,333,334],{"class":241},"=",[236,336,337],{"class":245}," msal.ConfidentialClientApplication(os.environ[",[236,339,340],{"class":277},"\"GRAPH_CLIENT_ID\"",[236,342,343],{"class":245},"], ",[236,345,347],{"class":346},"sa561","authority",[236,349,334],{"class":241},[236,351,268],{"class":267},[236,353,354],{"class":245},",\n",[236,356,358,361,363,365,368],{"class":61,"line":357},8,[236,359,360],{"class":346},"                                             client_credential",[236,362,334],{"class":241},[236,364,285],{"class":245},[236,366,367],{"class":277},"\"GRAPH_CLIENT_SECRET\"",[236,369,370],{"class":245},"])\n",[236,372,374,377,380,383,385,388,391,394,397],{"class":61,"line":373},9,[236,375,376],{"class":241},"    return",[236,378,379],{"class":245}," app.acquire_token_for_client(",[236,381,382],{"class":346},"scopes",[236,384,334],{"class":241},[236,386,387],{"class":245},"[",[236,389,390],{"class":277},"\"https:\u002F\u002Fgraph.microsoft.com\u002F.default\"",[236,392,393],{"class":245},"])[",[236,395,396],{"class":277},"\"access_token\"",[236,398,399],{"class":245},"]\n",[236,401,403],{"class":61,"line":402},10,[236,404,261],{"emptyLinePlaceholder":260},[236,406,408,410,413,416,418,421],{"class":61,"line":407},11,[236,409,308],{"class":241},[236,411,412],{"class":311}," delegated_token",[236,414,415],{"class":245},"(cache: msal.SerializableTokenCache) -> ",[236,417,318],{"class":267},[236,419,420],{"class":245},":   ",[236,422,423],{"class":324},"# workbook API: ranges, tables, calculate\n",[236,425,427,429,431,434,436,438,440,442,444,447,450,452],{"class":61,"line":426},12,[236,428,331],{"class":245},[236,430,334],{"class":241},[236,432,433],{"class":245}," msal.PublicClientApplication(os.environ[",[236,435,340],{"class":277},[236,437,343],{"class":245},[236,439,347],{"class":346},[236,441,334],{"class":241},[236,443,268],{"class":267},[236,445,446],{"class":245},", ",[236,448,449],{"class":346},"token_cache",[236,451,334],{"class":241},[236,453,454],{"class":245},"cache)\n",[236,456,458,461,463],{"class":61,"line":457},13,[236,459,460],{"class":245},"    accounts ",[236,462,334],{"class":241},[236,464,465],{"class":245}," app.get_accounts()\n",[236,467,469,472,474,477,480,482,485,487,490,492,495,498,501,504],{"class":61,"line":468},14,[236,470,471],{"class":245},"    result ",[236,473,334],{"class":241},[236,475,476],{"class":245}," app.acquire_token_silent([",[236,478,479],{"class":277},"\"Files.ReadWrite\"",[236,481,343],{"class":245},[236,483,484],{"class":346},"account",[236,486,334],{"class":241},[236,488,489],{"class":245},"accounts[",[236,491,41],{"class":267},[236,493,494],{"class":245},"]) ",[236,496,497],{"class":241},"if",[236,499,500],{"class":245}," accounts ",[236,502,503],{"class":241},"else",[236,505,506],{"class":267}," None\n",[236,508,510,513,516],{"class":61,"line":509},15,[236,511,512],{"class":241},"    if",[236,514,515],{"class":241}," not",[236,517,518],{"class":245}," result:\n",[236,520,522,525,528,531,534],{"class":61,"line":521},16,[236,523,524],{"class":241},"        raise",[236,526,527],{"class":267}," RuntimeError",[236,529,530],{"class":245},"(",[236,532,533],{"class":277},"\"no cached sign-in: run the device code sign-in once by hand\"",[236,535,536],{"class":245},")\n",[236,538,540,542,545,547],{"class":61,"line":539},17,[236,541,376],{"class":241},[236,543,544],{"class":245}," result[",[236,546,396],{"class":277},[236,548,399],{"class":245},[236,550,552],{"class":61,"line":551},18,[236,553,261],{"emptyLinePlaceholder":260},[236,555,557,560,563,566,569,571],{"class":61,"line":556},19,[236,558,559],{"class":267},"print",[236,561,562],{"class":245},"(app_token()[:",[236,564,565],{"class":267},"8",[236,567,568],{"class":245},"], delegated_token(msal.SerializableTokenCache())[:",[236,570,565],{"class":267},[236,572,370],{"class":245},[10,574,575,576,203],{},"A delegated job still runs unattended once it has been set up: sign in once with the device code flow, persist MSAL's token cache as a protected file,\nand every later run acquires tokens silently. Use a dedicated account whose access is limited to the libraries the job touches, and make the script\nfail fast — never wait for a device code — when the cached sign-in has expired. The full setup, including cache protection and a token check, is in\n",[14,577,166],{"href":165},[20,579,29,585,29,588,29,591,29,595,29,602,29,606,29,612,29,615,29,619,29,623,29,627,29,630,29,635,29,637,29,641,29,643,29,645,29,647,29,649,29,652,29,655,29,657,29,660,29,662,29,666,29,668,29,670,29,672,29,676,29,678,29,681,29,683,29,685,29,687,29,689,29,692,29,696,29,698,29,701,29,705,29,709,29,711,29,713,29,715,29,718,29,720,29,723,29,725,29,727,29,729,29,731,29,734,29,738,29,740,29,743,29,745,29,747,29,749,29,753],{"viewBox":580,"role":23,"ariaLabelledBy":581,"xmlns":27,"style":584},"0 0 456 284",[582,583],"gxl-perms-t","gxl-perms-d","width:100%;max-width:456px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,586,587],{"id":582},"Token types accepted by the Graph operations used in this section",[35,589,590],{"id":583},"Downloading and uploading file content accept both app-only and delegated tokens. Range reads and writes, table row additions, sessions and calculation accept delegated tokens only; calculation additionally requires a work or school account.",[39,592],{"x":41,"y":41,"width":593,"height":594,"fill":44},"456","284",[39,596],{"x":48,"y":48,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[39,603],{"x":604,"y":48,"width":605,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"56","170",[56,607,611],{"x":608,"y":609,"style":610},"141","40","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[39,613],{"x":614,"y":48,"width":64,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"226",[56,616,618],{"x":617,"y":609,"style":610},"276","B",[39,620],{"x":621,"y":48,"width":622,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"326","110",[56,624,626],{"x":625,"y":609,"style":610},"381","C",[39,628],{"x":48,"y":629,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"50",[56,631,634],{"x":632,"y":633,"style":610},"38","70","1",[39,636],{"x":604,"y":629,"width":605,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},[56,638,640],{"x":608,"y":633,"style":639},"font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","operation",[39,642],{"x":614,"y":629,"width":64,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},[56,644,214],{"x":617,"y":633,"style":639},[39,646],{"x":621,"y":629,"width":622,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},[56,648,218],{"x":625,"y":633,"style":639},[39,650],{"x":48,"y":651,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"80",[56,653,654],{"x":632,"y":64,"style":610},"2",[39,656],{"x":604,"y":651,"width":605,"height":598,"rx":41,"fill":44,"stroke":600,"style":601},[56,658,659],{"x":608,"y":64,"style":97},"download file content",[39,661],{"x":614,"y":651,"width":64,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,663,665],{"x":617,"y":64,"style":664},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","yes",[39,667],{"x":621,"y":651,"width":622,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,669,665],{"x":625,"y":64,"style":664},[39,671],{"x":48,"y":622,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},[56,673,675],{"x":632,"y":674,"style":610},"130","3",[39,677],{"x":604,"y":622,"width":605,"height":598,"rx":41,"fill":44,"stroke":600,"style":601},[56,679,680],{"x":608,"y":674,"style":97},"upload file content",[39,682],{"x":614,"y":622,"width":64,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,684,665],{"x":617,"y":674,"style":664},[39,686],{"x":621,"y":622,"width":622,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,688,665],{"x":625,"y":674,"style":664},[39,690],{"x":48,"y":691,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"140",[56,693,695],{"x":632,"y":694,"style":610},"160","4",[39,697],{"x":604,"y":691,"width":605,"height":598,"rx":41,"fill":44,"stroke":600,"style":601},[56,699,700],{"x":608,"y":694,"style":97},"read \u002F write ranges",[39,702],{"x":614,"y":691,"width":64,"height":598,"rx":41,"fill":703,"stroke":704,"style":601},"#fee8f2","var(--accent,#f43f8f)",[56,706,708],{"x":617,"y":694,"style":707},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","no",[39,710],{"x":621,"y":691,"width":622,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,712,665],{"x":625,"y":694,"style":664},[39,714],{"x":48,"y":605,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},[56,716,717],{"x":632,"y":47,"style":610},"5",[39,719],{"x":604,"y":605,"width":605,"height":598,"rx":41,"fill":44,"stroke":600,"style":601},[56,721,722],{"x":608,"y":47,"style":97},"tables: rows\u002Fadd",[39,724],{"x":614,"y":605,"width":64,"height":598,"rx":41,"fill":703,"stroke":704,"style":601},[56,726,708],{"x":617,"y":47,"style":707},[39,728],{"x":621,"y":605,"width":622,"height":598,"rx":41,"fill":82,"stroke":83,"style":601},[56,730,665],{"x":625,"y":47,"style":664},[39,732],{"x":48,"y":733,"width":597,"height":598,"rx":41,"fill":599,"stroke":600,"style":601},"200",[56,735,737],{"x":632,"y":736,"style":610},"220","6",[39,739],{"x":604,"y":733,"width":605,"height":598,"rx":41,"fill":44,"stroke":600,"style":601},[56,741,742],{"x":608,"y":736,"style":97},"sessions, calculate",[39,744],{"x":614,"y":733,"width":64,"height":598,"rx":41,"fill":703,"stroke":704,"style":601},[56,746,708],{"x":617,"y":736,"style":707},[39,748],{"x":621,"y":733,"width":622,"height":598,"rx":41,"fill":132,"stroke":133,"style":601},[56,750,752],{"x":625,"y":736,"style":751},"font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","work account",[56,754,757],{"x":755,"y":756,"style":150},"228","262","check the permissions table on each Graph reference page",[153,759,761],{"id":760},"setting-up-the-app-registration","Setting up the app registration",[10,763,764],{},"Both token types come from a single app registration in Microsoft Entra ID, configured for the two roles it plays. Keeping them on one\nregistration is simpler to audit; splitting them into two registrations is reasonable when different teams own the read and write sides.",[766,767,768,784],"table",{},[769,770,771],"thead",{},[772,773,774,778,781],"tr",{},[775,776,777],"th",{},"Setting",[775,779,780],{},"For",[775,782,783],{},"Value",[785,786,787,805,816,833,844],"tbody",{},[772,788,789,793,795],{},[790,791,792],"td",{},"Delegated permission",[790,794,117],{},[790,796,797,800,801,804],{},[233,798,799],{},"Files.ReadWrite"," (or ",[233,802,803],{},"Files.ReadWrite.All"," for files shared by others)",[772,806,807,810,813],{},[790,808,809],{},"Allow public client flows",[790,811,812],{},"device code sign-in",[790,814,815],{},"Enabled",[772,817,818,821,824],{},[790,819,820],{},"Application permission",[790,822,823],{},"file download and upload",[790,825,826,829,830],{},[233,827,828],{},"Sites.Selected"," with a per-site grant, or ",[233,831,832],{},"Files.Read.All",[772,834,835,838,841],{},[790,836,837],{},"Admin consent",[790,839,840],{},"application permissions",[790,842,843],{},"Granted by a tenant administrator",[772,845,846,849,852],{},[790,847,848],{},"Client secret or certificate",[790,850,851],{},"app-only tokens",[790,853,854],{},"Stored outside the code, rotated on a schedule",[10,856,857],{},"Two operational decisions are worth making at the same time. Decide which account signs in for the delegated side — a dedicated service account is far easier to\nmanage than a person's account that disappears when they change roles — and confirm how your organisation's conditional access policies treat it, because a policy\nthat demands periodic re-authentication will eventually require a manual sign-in. And decide where the token cache and client secret live on the machine that runs\nthe job, with file permissions or a secret store that only the job's user can read.",[153,859,861],{"id":860},"addressing-a-workbook","Addressing a workbook",[10,863,864,865,868,869,872],{},"Every call starts from a drive item. SharePoint sites are resolved from their host name and path to a site ID, a document library is the site's\n",[233,866,867],{},"drive",", and files are addressed either by path under the drive root or by their stable item ID. A OneDrive is the same shape under\n",[233,870,871],{},"\u002Fusers\u002F{upn}\u002Fdrive",". Resolve IDs once and keep them in configuration; paths change when people rename folders, item IDs do not.",[226,874,876],{"className":228,"code":875,"language":230,"meta":231,"style":231},"import requests\nfrom urllib.parse import quote\n\nGRAPH = \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\nhttp = requests.Session()\nhttp.headers[\"Authorization\"] = f\"Bearer {app_token()}\"\n\nsite = http.get(f\"{GRAPH}\u002Fsites\u002Fcontoso.sharepoint.com:\u002Fsites\u002FFinance\", timeout=60).json()[\"id\"]\ndrive = http.get(f\"{GRAPH}\u002Fsites\u002F{site}\u002Fdrive\", timeout=60).json()[\"id\"]\nitem = http.get(f\"{GRAPH}\u002Fdrives\u002F{drive}\u002Froot:\u002F{quote('Reports\u002F2026\u002Fsales.xlsx')}\", timeout=60).json()\n\nBOOK = f\"{GRAPH}\u002Fdrives\u002F{drive}\u002Fitems\u002F{item['id']}\u002Fworkbook\"\nprint(item[\"name\"], item[\"eTag\"], BOOK)\n",[233,877,878,885,898,902,912,922,949,953,996,1042,1099,1103,1146],{"__ignoreMap":231},[236,879,880,882],{"class":61,"line":238},[236,881,242],{"class":241},[236,883,884],{"class":245}," requests\n",[236,886,887,890,893,895],{"class":61,"line":249},[236,888,889],{"class":241},"from",[236,891,892],{"class":245}," urllib.parse ",[236,894,242],{"class":241},[236,896,897],{"class":245}," quote\n",[236,899,900],{"class":61,"line":257},[236,901,261],{"emptyLinePlaceholder":260},[236,903,904,907,909],{"class":61,"line":264},[236,905,906],{"class":267},"GRAPH",[236,908,271],{"class":241},[236,910,911],{"class":277}," \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n",[236,913,914,917,919],{"class":61,"line":300},[236,915,916],{"class":245},"http ",[236,918,334],{"class":241},[236,920,921],{"class":245}," requests.Session()\n",[236,923,924,927,930,933,935,937,940,942,945,947],{"class":61,"line":305},[236,925,926],{"class":245},"http.headers[",[236,928,929],{"class":277},"\"Authorization\"",[236,931,932],{"class":245},"] ",[236,934,334],{"class":241},[236,936,274],{"class":241},[236,938,939],{"class":277},"\"Bearer ",[236,941,282],{"class":281},[236,943,944],{"class":245},"app_token()",[236,946,294],{"class":281},[236,948,297],{"class":277},[236,950,951],{"class":61,"line":328},[236,952,261],{"emptyLinePlaceholder":260},[236,954,955,958,960,963,966,969,971,973,975,978,980,983,985,988,991,994],{"class":61,"line":357},[236,956,957],{"class":245},"site ",[236,959,334],{"class":241},[236,961,962],{"class":245}," http.get(",[236,964,965],{"class":241},"f",[236,967,968],{"class":277},"\"",[236,970,282],{"class":281},[236,972,906],{"class":267},[236,974,294],{"class":281},[236,976,977],{"class":277},"\u002Fsites\u002Fcontoso.sharepoint.com:\u002Fsites\u002FFinance\"",[236,979,446],{"class":245},[236,981,982],{"class":346},"timeout",[236,984,334],{"class":241},[236,986,987],{"class":267},"60",[236,989,990],{"class":245},").json()[",[236,992,993],{"class":277},"\"id\"",[236,995,399],{"class":245},[236,997,998,1001,1003,1005,1007,1009,1011,1013,1015,1018,1020,1023,1025,1028,1030,1032,1034,1036,1038,1040],{"class":61,"line":373},[236,999,1000],{"class":245},"drive ",[236,1002,334],{"class":241},[236,1004,962],{"class":245},[236,1006,965],{"class":241},[236,1008,968],{"class":277},[236,1010,282],{"class":281},[236,1012,906],{"class":267},[236,1014,294],{"class":281},[236,1016,1017],{"class":277},"\u002Fsites\u002F",[236,1019,282],{"class":281},[236,1021,1022],{"class":245},"site",[236,1024,294],{"class":281},[236,1026,1027],{"class":277},"\u002Fdrive\"",[236,1029,446],{"class":245},[236,1031,982],{"class":346},[236,1033,334],{"class":241},[236,1035,987],{"class":267},[236,1037,990],{"class":245},[236,1039,993],{"class":277},[236,1041,399],{"class":245},[236,1043,1044,1047,1049,1051,1053,1055,1057,1059,1061,1064,1066,1068,1070,1073,1075,1078,1081,1084,1086,1088,1090,1092,1094,1096],{"class":61,"line":402},[236,1045,1046],{"class":245},"item ",[236,1048,334],{"class":241},[236,1050,962],{"class":245},[236,1052,965],{"class":241},[236,1054,968],{"class":277},[236,1056,282],{"class":281},[236,1058,906],{"class":267},[236,1060,294],{"class":281},[236,1062,1063],{"class":277},"\u002Fdrives\u002F",[236,1065,282],{"class":281},[236,1067,867],{"class":245},[236,1069,294],{"class":281},[236,1071,1072],{"class":277},"\u002Froot:\u002F",[236,1074,282],{"class":281},[236,1076,1077],{"class":245},"quote(",[236,1079,1080],{"class":277},"'Reports\u002F2026\u002Fsales.xlsx'",[236,1082,1083],{"class":245},")",[236,1085,294],{"class":281},[236,1087,968],{"class":277},[236,1089,446],{"class":245},[236,1091,982],{"class":346},[236,1093,334],{"class":241},[236,1095,987],{"class":267},[236,1097,1098],{"class":245},").json()\n",[236,1100,1101],{"class":61,"line":407},[236,1102,261],{"emptyLinePlaceholder":260},[236,1104,1105,1108,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1131,1133,1136,1139,1141,1143],{"class":61,"line":426},[236,1106,1107],{"class":267},"BOOK",[236,1109,271],{"class":241},[236,1111,274],{"class":241},[236,1113,968],{"class":277},[236,1115,282],{"class":281},[236,1117,906],{"class":267},[236,1119,294],{"class":281},[236,1121,1063],{"class":277},[236,1123,282],{"class":281},[236,1125,867],{"class":245},[236,1127,294],{"class":281},[236,1129,1130],{"class":277},"\u002Fitems\u002F",[236,1132,282],{"class":281},[236,1134,1135],{"class":245},"item[",[236,1137,1138],{"class":277},"'id'",[236,1140,291],{"class":245},[236,1142,294],{"class":281},[236,1144,1145],{"class":277},"\u002Fworkbook\"\n",[236,1147,1148,1150,1153,1156,1159,1162,1164,1166],{"class":61,"line":457},[236,1149,559],{"class":267},[236,1151,1152],{"class":245},"(item[",[236,1154,1155],{"class":277},"\"name\"",[236,1157,1158],{"class":245},"], item[",[236,1160,1161],{"class":277},"\"eTag\"",[236,1163,343],{"class":245},[236,1165,1107],{"class":267},[236,1167,536],{"class":245},[10,1169,1170,1171,1174],{},"The item metadata is useful on its own. Its ",[233,1172,1173],{},"eTag"," changes whenever the file changes, so comparing it with the last value your job processed is a cheap\nway to skip work when a source file has not been touched since the previous run.",[153,1176,1178],{"id":1177},"reading-source-data-into-pandas","Reading source data into pandas",[10,1180,1181,1182,1185,1186,1189,1190,1193],{},"When a job needs the contents of a workbook — every row of a sheet, several sheets, a folder of branch files — downloading the file is simpler and faster\nthan asking the workbook API for ranges. Request ",[233,1183,1184],{},":\u002Fcontent"," for the item, wrap the bytes in ",[233,1187,1188],{},"BytesIO",", and every ",[233,1191,1192],{},"read_excel"," option applies.",[226,1195,1197],{"className":228,"code":1196,"language":230,"meta":231,"style":231},"from io import BytesIO\nimport pandas as pd\n\ncontent = http.get(f\"{GRAPH}\u002Fdrives\u002F{drive}\u002Fitems\u002F{item['id']}\u002Fcontent\", timeout=300).content\nframes = pd.read_excel(BytesIO(content), sheet_name=None)\nfor name, frame in frames.items():\n    print(name, frame.shape)\n",[233,1198,1199,1211,1224,1228,1282,1302,1316],{"__ignoreMap":231},[236,1200,1201,1203,1206,1208],{"class":61,"line":238},[236,1202,889],{"class":241},[236,1204,1205],{"class":245}," io ",[236,1207,242],{"class":241},[236,1209,1210],{"class":245}," BytesIO\n",[236,1212,1213,1215,1218,1221],{"class":61,"line":249},[236,1214,242],{"class":241},[236,1216,1217],{"class":245}," pandas ",[236,1219,1220],{"class":241},"as",[236,1222,1223],{"class":245}," pd\n",[236,1225,1226],{"class":61,"line":257},[236,1227,261],{"emptyLinePlaceholder":260},[236,1229,1230,1233,1235,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1270,1272,1274,1276,1279],{"class":61,"line":264},[236,1231,1232],{"class":245},"content ",[236,1234,334],{"class":241},[236,1236,962],{"class":245},[236,1238,965],{"class":241},[236,1240,968],{"class":277},[236,1242,282],{"class":281},[236,1244,906],{"class":267},[236,1246,294],{"class":281},[236,1248,1063],{"class":277},[236,1250,282],{"class":281},[236,1252,867],{"class":245},[236,1254,294],{"class":281},[236,1256,1130],{"class":277},[236,1258,282],{"class":281},[236,1260,1135],{"class":245},[236,1262,1138],{"class":277},[236,1264,291],{"class":245},[236,1266,294],{"class":281},[236,1268,1269],{"class":277},"\u002Fcontent\"",[236,1271,446],{"class":245},[236,1273,982],{"class":346},[236,1275,334],{"class":241},[236,1277,1278],{"class":267},"300",[236,1280,1281],{"class":245},").content\n",[236,1283,1284,1287,1289,1292,1295,1297,1300],{"class":61,"line":300},[236,1285,1286],{"class":245},"frames ",[236,1288,334],{"class":241},[236,1290,1291],{"class":245}," pd.read_excel(BytesIO(content), ",[236,1293,1294],{"class":346},"sheet_name",[236,1296,334],{"class":241},[236,1298,1299],{"class":267},"None",[236,1301,536],{"class":245},[236,1303,1304,1307,1310,1313],{"class":61,"line":305},[236,1305,1306],{"class":241},"for",[236,1308,1309],{"class":245}," name, frame ",[236,1311,1312],{"class":241},"in",[236,1314,1315],{"class":245}," frames.items():\n",[236,1317,1318,1321],{"class":61,"line":328},[236,1319,1320],{"class":267},"    print",[236,1322,1323],{"class":245},"(name, frame.shape)\n",[10,1325,1326],{},"Reading the whole file this way has a second advantage beyond speed: the job works on a consistent snapshot. Several range requests\nmade one after another can straddle an edit somebody makes in between, so the first sheet reflects the old numbers and the second the new\nones. A single download cannot, which matters whenever a report reconciles figures across sheets.",[10,1328,1329,1330,1333,1334,203],{},"This path works with an app-only token and reads the last saved version, which in Excel for the web is continuously autosaved. For folders, follow\n",[233,1331,1332],{},"@odata.nextLink"," when listing children; for very large files, stream the response to a temporary file instead of holding it in memory. Each of those,\nwith site resolution and eTag checks, is in\n",[14,1335,173],{"href":172},[153,1337,1339],{"id":1338},"editing-ranges-in-place","Editing ranges in place",[10,1341,1342,1343,1346,1347,1350],{},"Writing back is where the routes diverge sharply. Uploading a modified copy replaces the whole file and races with anyone who has it open. The workbook\nAPI edits just the range you name, inside the online service, so co-authors see new values appear and formulas recalculate. The request is a ",[233,1344,1345],{},"PATCH"," with a\n",[233,1348,1349],{},"values"," array whose dimensions exactly match the address.",[226,1352,1354],{"className":228,"code":1353,"language":230,"meta":231,"style":231},"http.headers[\"Authorization\"] = \"Bearer \" + delegated_token(msal.SerializableTokenCache())\n\nsummary = pd.DataFrame({\"Region\": [\"North\", \"South\"], \"Amount\": [2000.0, 1600.0]})\nvalues = [list(summary.columns)] + [[v.item() if hasattr(v, \"item\") else v for v in row]\n                                    for row in summary.itertuples(index=False)]\naddress = f\"A1:B{len(values)}\"\nresponse = http.patch(f\"{BOOK}\u002Fworksheets\u002F{quote('Summary')}\u002Frange(address='{address}')\",\n                      json={\"values\": values}, timeout=60)\nprint(response.status_code, address)\n",[233,1355,1356,1375,1379,1421,1471,1495,1519,1568,1591],{"__ignoreMap":231},[236,1357,1358,1360,1362,1364,1366,1369,1372],{"class":61,"line":238},[236,1359,926],{"class":245},[236,1361,929],{"class":277},[236,1363,932],{"class":245},[236,1365,334],{"class":241},[236,1367,1368],{"class":277}," \"Bearer \"",[236,1370,1371],{"class":241}," +",[236,1373,1374],{"class":245}," delegated_token(msal.SerializableTokenCache())\n",[236,1376,1377],{"class":61,"line":249},[236,1378,261],{"emptyLinePlaceholder":260},[236,1380,1381,1384,1386,1389,1392,1395,1398,1400,1403,1405,1408,1410,1413,1415,1418],{"class":61,"line":257},[236,1382,1383],{"class":245},"summary ",[236,1385,334],{"class":241},[236,1387,1388],{"class":245}," pd.DataFrame({",[236,1390,1391],{"class":277},"\"Region\"",[236,1393,1394],{"class":245},": [",[236,1396,1397],{"class":277},"\"North\"",[236,1399,446],{"class":245},[236,1401,1402],{"class":277},"\"South\"",[236,1404,343],{"class":245},[236,1406,1407],{"class":277},"\"Amount\"",[236,1409,1394],{"class":245},[236,1411,1412],{"class":267},"2000.0",[236,1414,446],{"class":245},[236,1416,1417],{"class":267},"1600.0",[236,1419,1420],{"class":245},"]})\n",[236,1422,1423,1426,1428,1431,1434,1437,1440,1443,1445,1448,1451,1454,1457,1459,1462,1464,1466,1468],{"class":61,"line":264},[236,1424,1425],{"class":245},"values ",[236,1427,334],{"class":241},[236,1429,1430],{"class":245}," [",[236,1432,1433],{"class":267},"list",[236,1435,1436],{"class":245},"(summary.columns)] ",[236,1438,1439],{"class":241},"+",[236,1441,1442],{"class":245}," [[v.item() ",[236,1444,497],{"class":241},[236,1446,1447],{"class":267}," hasattr",[236,1449,1450],{"class":245},"(v, ",[236,1452,1453],{"class":277},"\"item\"",[236,1455,1456],{"class":245},") ",[236,1458,503],{"class":241},[236,1460,1461],{"class":245}," v ",[236,1463,1306],{"class":241},[236,1465,1461],{"class":245},[236,1467,1312],{"class":241},[236,1469,1470],{"class":245}," row]\n",[236,1472,1473,1476,1479,1481,1484,1487,1489,1492],{"class":61,"line":300},[236,1474,1475],{"class":241},"                                    for",[236,1477,1478],{"class":245}," row ",[236,1480,1312],{"class":241},[236,1482,1483],{"class":245}," summary.itertuples(",[236,1485,1486],{"class":346},"index",[236,1488,334],{"class":241},[236,1490,1491],{"class":267},"False",[236,1493,1494],{"class":245},")]\n",[236,1496,1497,1500,1502,1504,1507,1509,1512,1515,1517],{"class":61,"line":305},[236,1498,1499],{"class":245},"address ",[236,1501,334],{"class":241},[236,1503,274],{"class":241},[236,1505,1506],{"class":277},"\"A1:B",[236,1508,282],{"class":281},[236,1510,1511],{"class":267},"len",[236,1513,1514],{"class":245},"(values)",[236,1516,294],{"class":281},[236,1518,297],{"class":277},[236,1520,1521,1524,1526,1529,1531,1533,1535,1537,1539,1542,1544,1546,1549,1551,1553,1556,1558,1561,1563,1566],{"class":61,"line":328},[236,1522,1523],{"class":245},"response ",[236,1525,334],{"class":241},[236,1527,1528],{"class":245}," http.patch(",[236,1530,965],{"class":241},[236,1532,968],{"class":277},[236,1534,282],{"class":281},[236,1536,1107],{"class":267},[236,1538,294],{"class":281},[236,1540,1541],{"class":277},"\u002Fworksheets\u002F",[236,1543,282],{"class":281},[236,1545,1077],{"class":245},[236,1547,1548],{"class":277},"'Summary'",[236,1550,1083],{"class":245},[236,1552,294],{"class":281},[236,1554,1555],{"class":277},"\u002Frange(address='",[236,1557,282],{"class":281},[236,1559,1560],{"class":245},"address",[236,1562,294],{"class":281},[236,1564,1565],{"class":277},"')\"",[236,1567,354],{"class":245},[236,1569,1570,1573,1575,1577,1580,1583,1585,1587,1589],{"class":61,"line":357},[236,1571,1572],{"class":346},"                      json",[236,1574,334],{"class":241},[236,1576,282],{"class":245},[236,1578,1579],{"class":277},"\"values\"",[236,1581,1582],{"class":245},": values}, ",[236,1584,982],{"class":346},[236,1586,334],{"class":241},[236,1588,987],{"class":267},[236,1590,536],{"class":245},[236,1592,1593,1595],{"class":61,"line":373},[236,1594,559],{"class":267},[236,1596,1597],{"class":245},"(response.status_code, address)\n",[10,1599,1600,1601,1604,1605,1607,1608,1611,1612,1615,1616,1619,1620,1623,1624,1627,1628,203],{},"Three details cause most of the trouble with range updates, and all three are silent. A ",[233,1602,1603],{},"null"," in ",[233,1606,1349],{}," means ",[221,1609,1610],{},"leave this cell unchanged",", not ",[221,1613,1614],{},"clear it",",\nso send an empty string for blanks. NumPy scalars are not JSON-serialisable, so convert with ",[233,1617,1618],{},".item()",". And ISO date strings are parsed according to the\nworkbook's locale, so send Excel serial numbers with a date ",[233,1621,1622],{},"numberFormat"," instead. Reading works the same way in reverse, with\n",[233,1625,1626],{},"usedRange(valuesOnly=true)"," for sheets whose size is not known. The complete treatment is in\n",[14,1629,180],{"href":179},[20,1631,29,1636,29,1639,29,1642,29,1645,29,1649,29,1654,29,1657,29,1661,29,1665,29,1668,29,1670,29,1674,29,1678,29,1681,29,1684,29,1687,29,1689,29,1693,29,1697,29,1700,29,1703,29,1706],{"viewBox":1632,"role":23,"ariaLabelledBy":1633,"xmlns":27,"style":28},"0 0 760 191",[1634,1635],"gxl-traps-t","gxl-traps-d",[31,1637,1638],{"id":1634},"Silent traps in workbook API writes",[35,1640,1641],{"id":1635},"Null values leave cells unchanged instead of clearing them, NumPy numbers cannot be encoded as JSON, and date strings are interpreted by locale. Send empty strings, plain Python numbers, and serial numbers with a date format.",[39,1643],{"x":41,"y":41,"width":42,"height":1644,"fill":44},"191",[39,1646],{"x":48,"y":48,"width":80,"height":1647,"rx":1648,"fill":703,"stroke":704,"style":54},"127","14",[56,1650,1653],{"x":67,"y":1651,"style":1652},"46","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","null means skip",[61,1655],{"x1":597,"y1":604,"x2":1656,"y2":604,"stroke":704,"style":601},"230.7",[56,1658,1660],{"x":67,"y":1659,"style":97},"78","NaN sent as null",[56,1662,1664],{"x":67,"y":1663,"style":97},"101","old value stays",[56,1666,1667],{"x":67,"y":71,"style":97},"send \"\" to clear",[39,1669],{"x":110,"y":48,"width":80,"height":1647,"rx":1648,"fill":132,"stroke":133,"style":54},[56,1671,1673],{"x":49,"y":1651,"style":1672},"font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","NumPy scalars",[61,1675],{"x1":1676,"y1":604,"x2":1677,"y2":604,"stroke":133,"style":601},"282.7","477.3",[56,1679,1680],{"x":49,"y":1659,"style":97},"int64, float64",[56,1682,1683],{"x":49,"y":1663,"style":97},"not JSON-serialisable",[56,1685,1686],{"x":49,"y":71,"style":97},"convert with .item()",[39,1688],{"x":131,"y":48,"width":80,"height":1647,"rx":1648,"fill":52,"stroke":53,"style":54},[56,1690,1692],{"x":68,"y":1651,"style":1691},"font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","date strings",[61,1694],{"x1":1695,"y1":604,"x2":1696,"y2":604,"stroke":53,"style":601},"529.3","724",[56,1698,1699],{"x":68,"y":1659,"style":97},"\"2026-03-04\"",[56,1701,1702],{"x":68,"y":1663,"style":97},"parsed by locale",[56,1704,1705],{"x":68,"y":71,"style":97},"serial + numberFormat",[56,1707,1709],{"x":49,"y":1708,"style":150},"177","none of these raise an error from Graph; they just write the wrong thing",[153,1711,1713],{"id":1712},"feeding-shared-tables","Feeding shared tables",[10,1715,1716,1717,1720],{},"When the destination is an Excel table — the usual structure behind a shared tracker, a Power BI source or a Power Automate trigger — append with\n",[233,1718,1719],{},"tables\u002F{name}\u002Frows\u002Fadd"," rather than computing addresses. The table grows, calculated columns and formatting extend, and the job never needs to know which\nrow is the last one.",[226,1722,1724],{"className":228,"code":1723,"language":230,"meta":231,"style":231},"TABLE = f\"{BOOK}\u002Ftables\u002F{quote('Orders')}\"\nheader = http.get(f\"{TABLE}\u002FheaderRowRange\", timeout=60).json()[\"values\"][0]\n\nincoming = pd.DataFrame({\"Amount\": [1320.0], \"Rep\": [\"Cara\"], \"Region\": [\"West\"]})\nrows = [[v.item() if hasattr(v, \"item\") else v for v in r] for r in incoming[header].itertuples(index=False)]\nadded = http.post(f\"{TABLE}\u002Frows\u002Fadd\", json={\"index\": None, \"values\": rows}, timeout=120)\nprint(added.status_code, header)\n",[233,1725,1726,1759,1800,1804,1841,1891,1947],{"__ignoreMap":231},[236,1727,1728,1731,1733,1735,1737,1739,1741,1743,1746,1748,1750,1753,1755,1757],{"class":61,"line":238},[236,1729,1730],{"class":267},"TABLE",[236,1732,271],{"class":241},[236,1734,274],{"class":241},[236,1736,968],{"class":277},[236,1738,282],{"class":281},[236,1740,1107],{"class":267},[236,1742,294],{"class":281},[236,1744,1745],{"class":277},"\u002Ftables\u002F",[236,1747,282],{"class":281},[236,1749,1077],{"class":245},[236,1751,1752],{"class":277},"'Orders'",[236,1754,1083],{"class":245},[236,1756,294],{"class":281},[236,1758,297],{"class":277},[236,1760,1761,1764,1766,1768,1770,1772,1774,1776,1778,1781,1783,1785,1787,1789,1791,1793,1796,1798],{"class":61,"line":249},[236,1762,1763],{"class":245},"header ",[236,1765,334],{"class":241},[236,1767,962],{"class":245},[236,1769,965],{"class":241},[236,1771,968],{"class":277},[236,1773,282],{"class":281},[236,1775,1730],{"class":267},[236,1777,294],{"class":281},[236,1779,1780],{"class":277},"\u002FheaderRowRange\"",[236,1782,446],{"class":245},[236,1784,982],{"class":346},[236,1786,334],{"class":241},[236,1788,987],{"class":267},[236,1790,990],{"class":245},[236,1792,1579],{"class":277},[236,1794,1795],{"class":245},"][",[236,1797,41],{"class":267},[236,1799,399],{"class":245},[236,1801,1802],{"class":61,"line":257},[236,1803,261],{"emptyLinePlaceholder":260},[236,1805,1806,1809,1811,1813,1815,1817,1820,1822,1825,1827,1830,1832,1834,1836,1839],{"class":61,"line":264},[236,1807,1808],{"class":245},"incoming ",[236,1810,334],{"class":241},[236,1812,1388],{"class":245},[236,1814,1407],{"class":277},[236,1816,1394],{"class":245},[236,1818,1819],{"class":267},"1320.0",[236,1821,343],{"class":245},[236,1823,1824],{"class":277},"\"Rep\"",[236,1826,1394],{"class":245},[236,1828,1829],{"class":277},"\"Cara\"",[236,1831,343],{"class":245},[236,1833,1391],{"class":277},[236,1835,1394],{"class":245},[236,1837,1838],{"class":277},"\"West\"",[236,1840,1420],{"class":245},[236,1842,1843,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864,1866,1868,1870,1873,1875,1878,1880,1883,1885,1887,1889],{"class":61,"line":300},[236,1844,1845],{"class":245},"rows ",[236,1847,334],{"class":241},[236,1849,1442],{"class":245},[236,1851,497],{"class":241},[236,1853,1447],{"class":267},[236,1855,1450],{"class":245},[236,1857,1453],{"class":277},[236,1859,1456],{"class":245},[236,1861,503],{"class":241},[236,1863,1461],{"class":245},[236,1865,1306],{"class":241},[236,1867,1461],{"class":245},[236,1869,1312],{"class":241},[236,1871,1872],{"class":245}," r] ",[236,1874,1306],{"class":241},[236,1876,1877],{"class":245}," r ",[236,1879,1312],{"class":241},[236,1881,1882],{"class":245}," incoming[header].itertuples(",[236,1884,1486],{"class":346},[236,1886,334],{"class":241},[236,1888,1491],{"class":267},[236,1890,1494],{"class":245},[236,1892,1893,1896,1898,1901,1903,1905,1907,1909,1911,1914,1916,1919,1921,1923,1926,1929,1931,1933,1935,1938,1940,1942,1945],{"class":61,"line":305},[236,1894,1895],{"class":245},"added ",[236,1897,334],{"class":241},[236,1899,1900],{"class":245}," http.post(",[236,1902,965],{"class":241},[236,1904,968],{"class":277},[236,1906,282],{"class":281},[236,1908,1730],{"class":267},[236,1910,294],{"class":281},[236,1912,1913],{"class":277},"\u002Frows\u002Fadd\"",[236,1915,446],{"class":245},[236,1917,1918],{"class":346},"json",[236,1920,334],{"class":241},[236,1922,282],{"class":245},[236,1924,1925],{"class":277},"\"index\"",[236,1927,1928],{"class":245},": ",[236,1930,1299],{"class":267},[236,1932,446],{"class":245},[236,1934,1579],{"class":277},[236,1936,1937],{"class":245},": rows}, ",[236,1939,982],{"class":346},[236,1941,334],{"class":241},[236,1943,1944],{"class":267},"120",[236,1946,536],{"class":245},[236,1948,1949,1951],{"class":61,"line":328},[236,1950,559],{"class":267},[236,1952,1953],{"class":245},"(added.status_code, header)\n",[10,1955,1956,1957,1960,1961,1964,1965,203],{},"Three habits turn that call into a dependable feed. Reorder the frame to the table's header, because the arrays are positional. Check existing keys before\nappending, so a rerun never duplicates rows. And send rows in batches — Microsoft's reference for this endpoint warns that one row per call degrades\nperformance — retrying ",[233,1958,1959],{},"504"," and ",[233,1962,1963],{},"429"," responses with a delay. The complete job is in\n",[14,1966,187],{"href":186},[153,1968,1970],{"id":1969},"sessions-and-calculation","Sessions and calculation",[10,1972,1973,1974,1977,1978,1981,1982,1985],{},"Each sessionless workbook request opens the workbook, performs the operation, saves and closes it. For a job of more than two or three calls, create a\n",[212,1975,1976],{},"session"," and send its ID in the ",[233,1979,1980],{},"workbook-session-id"," header: the workbook stays open between calls, and you choose whether changes persist. A\nnon-persistent session is the key to using a shared Excel model as a calculation service — write inputs, call ",[233,1983,1984],{},"application\u002Fcalculate",", read outputs, and let\nthe session end without anything being saved.",[226,1987,1989],{"className":228,"code":1988,"language":230,"meta":231,"style":231},"created = http.post(f\"{BOOK}\u002FcreateSession\", json={\"persistChanges\": False}, timeout=120).json()\nhttp.headers[\"workbook-session-id\"] = created[\"id\"]\ntry:\n    http.patch(f\"{BOOK}\u002Fworksheets\u002FInputs\u002Frange(address='B2')\", json={\"values\": [[0.07]]}, timeout=60)\n    http.post(f\"{BOOK}\u002Fapplication\u002Fcalculate\", json={\"calculationType\": \"Recalculate\"}, timeout=120)\n    result = http.get(f\"{BOOK}\u002Fworksheets\u002FModel\u002Frange(address='A1:B3')\", timeout=60).json()[\"values\"]\n    print(result)\nfinally:\n    http.post(f\"{BOOK}\u002FcloseSession\", json={}, timeout=60)\n    http.headers.pop(\"workbook-session-id\", None)\n",[233,1990,1991,2039,2057,2065,2110,2154,2189,2196,2203,2237],{"__ignoreMap":231},[236,1992,1993,1996,1998,2000,2002,2004,2006,2008,2010,2013,2015,2017,2019,2021,2024,2026,2028,2031,2033,2035,2037],{"class":61,"line":238},[236,1994,1995],{"class":245},"created ",[236,1997,334],{"class":241},[236,1999,1900],{"class":245},[236,2001,965],{"class":241},[236,2003,968],{"class":277},[236,2005,282],{"class":281},[236,2007,1107],{"class":267},[236,2009,294],{"class":281},[236,2011,2012],{"class":277},"\u002FcreateSession\"",[236,2014,446],{"class":245},[236,2016,1918],{"class":346},[236,2018,334],{"class":241},[236,2020,282],{"class":245},[236,2022,2023],{"class":277},"\"persistChanges\"",[236,2025,1928],{"class":245},[236,2027,1491],{"class":267},[236,2029,2030],{"class":245},"}, ",[236,2032,982],{"class":346},[236,2034,334],{"class":241},[236,2036,1944],{"class":267},[236,2038,1098],{"class":245},[236,2040,2041,2043,2046,2048,2050,2053,2055],{"class":61,"line":249},[236,2042,926],{"class":245},[236,2044,2045],{"class":277},"\"workbook-session-id\"",[236,2047,932],{"class":245},[236,2049,334],{"class":241},[236,2051,2052],{"class":245}," created[",[236,2054,993],{"class":277},[236,2056,399],{"class":245},[236,2058,2059,2062],{"class":61,"line":257},[236,2060,2061],{"class":241},"try",[236,2063,2064],{"class":245},":\n",[236,2066,2067,2070,2072,2074,2076,2078,2080,2083,2085,2087,2089,2091,2093,2096,2099,2102,2104,2106,2108],{"class":61,"line":264},[236,2068,2069],{"class":245},"    http.patch(",[236,2071,965],{"class":241},[236,2073,968],{"class":277},[236,2075,282],{"class":281},[236,2077,1107],{"class":267},[236,2079,294],{"class":281},[236,2081,2082],{"class":277},"\u002Fworksheets\u002FInputs\u002Frange(address='B2')\"",[236,2084,446],{"class":245},[236,2086,1918],{"class":346},[236,2088,334],{"class":241},[236,2090,282],{"class":245},[236,2092,1579],{"class":277},[236,2094,2095],{"class":245},": [[",[236,2097,2098],{"class":267},"0.07",[236,2100,2101],{"class":245},"]]}, ",[236,2103,982],{"class":346},[236,2105,334],{"class":241},[236,2107,987],{"class":267},[236,2109,536],{"class":245},[236,2111,2112,2115,2117,2119,2121,2123,2125,2128,2130,2132,2134,2136,2139,2141,2144,2146,2148,2150,2152],{"class":61,"line":300},[236,2113,2114],{"class":245},"    http.post(",[236,2116,965],{"class":241},[236,2118,968],{"class":277},[236,2120,282],{"class":281},[236,2122,1107],{"class":267},[236,2124,294],{"class":281},[236,2126,2127],{"class":277},"\u002Fapplication\u002Fcalculate\"",[236,2129,446],{"class":245},[236,2131,1918],{"class":346},[236,2133,334],{"class":241},[236,2135,282],{"class":245},[236,2137,2138],{"class":277},"\"calculationType\"",[236,2140,1928],{"class":245},[236,2142,2143],{"class":277},"\"Recalculate\"",[236,2145,2030],{"class":245},[236,2147,982],{"class":346},[236,2149,334],{"class":241},[236,2151,1944],{"class":267},[236,2153,536],{"class":245},[236,2155,2156,2158,2160,2162,2164,2166,2168,2170,2172,2175,2177,2179,2181,2183,2185,2187],{"class":61,"line":305},[236,2157,471],{"class":245},[236,2159,334],{"class":241},[236,2161,962],{"class":245},[236,2163,965],{"class":241},[236,2165,968],{"class":277},[236,2167,282],{"class":281},[236,2169,1107],{"class":267},[236,2171,294],{"class":281},[236,2173,2174],{"class":277},"\u002Fworksheets\u002FModel\u002Frange(address='A1:B3')\"",[236,2176,446],{"class":245},[236,2178,982],{"class":346},[236,2180,334],{"class":241},[236,2182,987],{"class":267},[236,2184,990],{"class":245},[236,2186,1579],{"class":277},[236,2188,399],{"class":245},[236,2190,2191,2193],{"class":61,"line":328},[236,2192,1320],{"class":267},[236,2194,2195],{"class":245},"(result)\n",[236,2197,2198,2201],{"class":61,"line":357},[236,2199,2200],{"class":241},"finally",[236,2202,2064],{"class":245},[236,2204,2205,2207,2209,2211,2213,2215,2217,2220,2222,2224,2226,2229,2231,2233,2235],{"class":61,"line":373},[236,2206,2114],{"class":245},[236,2208,965],{"class":241},[236,2210,968],{"class":277},[236,2212,282],{"class":281},[236,2214,1107],{"class":267},[236,2216,294],{"class":281},[236,2218,2219],{"class":277},"\u002FcloseSession\"",[236,2221,446],{"class":245},[236,2223,1918],{"class":346},[236,2225,334],{"class":241},[236,2227,2228],{"class":245},"{}, ",[236,2230,982],{"class":346},[236,2232,334],{"class":241},[236,2234,987],{"class":267},[236,2236,536],{"class":245},[236,2238,2239,2242,2244,2246,2248],{"class":61,"line":402},[236,2240,2241],{"class":245},"    http.headers.pop(",[236,2243,2045],{"class":277},[236,2245,446],{"class":245},[236,2247,1299],{"class":267},[236,2249,536],{"class":245},[10,2251,2252,2253,2256,2257,2260,2261,1960,2264,2267,2268,203],{},"Sessions expire after a few minutes without requests, so refresh or recreate them across long pauses. Opening a very large workbook can take long enough to\ntime out, and session creation supports ",[233,2254,2255],{},"Prefer: respond-async"," with status polling for exactly that case. ",[233,2258,2259],{},"Recalculate"," is the right calculation type almost\nalways; ",[233,2262,2263],{},"Full",[233,2265,2266],{},"FullRebuild"," exist for models whose dependencies ordinary recalculation misses. The patterns — a context manager, scenario loops and\nasynchronous session creation — are in\n",[14,2269,194],{"href":193},[20,2271,29,2276,29,2279,29,2282,29,2285,29,2289,29,2294,29,2297,29,2302,29,2305,29,2308,29,2312,29,2315,29,2319,29,2322,29,2325,29,2328,29,2331,29,2335,29,2339,29,2342,29,2345,29,2348,29,2351,29,2354,29,2358,29,2361,29,2364,29,2368,29,2371],{"viewBox":2272,"role":23,"ariaLabelledBy":2273,"xmlns":27,"style":28},"0 0 760 156",[2274,2275],"gxl-job-t","gxl-job-d",[31,2277,2278],{"id":2274},"A complete scheduled Graph job",[35,2280,2281],{"id":2275},"A scheduled job acquires tokens, checks whether the source workbook changed, downloads and transforms it with pandas, then opens a session on the destination workbook, appends table rows in batches, recalculates, and closes the session.",[39,2283],{"x":41,"y":41,"width":42,"height":2284,"fill":44},"156",[39,2286],{"x":48,"y":48,"width":2287,"height":2288,"rx":51,"fill":599,"stroke":600,"style":54},"121.6","86",[56,2290,2293],{"x":2291,"y":629,"style":2292},"80.8","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","tokens",[56,2295,2296],{"x":2291,"y":63,"style":639},"app + delegated",[61,2298],{"x1":2299,"y1":2300,"x2":2301,"y2":2300,"stroke":53,"style":54},"146.6","63","163.6",[73,2303],{"points":2304,"fill":76},"166.6,63 157.6,58 157.6,68",[39,2306],{"x":2307,"y":48,"width":2287,"height":2288,"rx":51,"fill":52,"stroke":53,"style":54},"169.6",[56,2309,2311],{"x":2310,"y":629,"style":58},"230.4","changed?",[56,2313,2314],{"x":2310,"y":63,"style":97},"eTag check",[61,2316],{"x1":2317,"y1":2300,"x2":2318,"y2":2300,"stroke":53,"style":54},"296.2","313.2",[73,2320],{"points":2321,"fill":76},"316.2,63 307.2,58 307.2,68",[39,2323],{"x":2324,"y":48,"width":2287,"height":2288,"rx":51,"fill":52,"stroke":53,"style":54},"319.2",[56,2326,2327],{"x":49,"y":629,"style":58},"pandas",[56,2329,2330],{"x":49,"y":63,"style":97},"download,",[56,2332,2334],{"x":49,"y":2333,"style":97},"92","transform",[61,2336],{"x1":2337,"y1":2300,"x2":2338,"y2":2300,"stroke":53,"style":54},"445.8","462.8",[73,2340],{"points":2341,"fill":76},"465.8,63 456.8,58 456.8,68",[39,2343],{"x":2344,"y":48,"width":2287,"height":2288,"rx":51,"fill":132,"stroke":133,"style":54},"468.8",[56,2346,1976],{"x":2347,"y":629,"style":140},"529.6",[56,2349,2350],{"x":2347,"y":63,"style":97},"rows\u002Fadd,",[56,2352,2353],{"x":2347,"y":2333,"style":97},"calculate",[61,2355],{"x1":2356,"y1":2300,"x2":2357,"y2":2300,"stroke":53,"style":54},"595.4","612.4",[73,2359],{"points":2360,"fill":76},"615.4,63 606.4,58 606.4,68",[39,2362],{"x":2363,"y":48,"width":2287,"height":2288,"rx":51,"fill":82,"stroke":83,"style":54},"618.4",[56,2365,2367],{"x":2366,"y":629,"style":92},"679.2","close + log",[56,2369,2370],{"x":2366,"y":63,"style":97},"eTag saved",[56,2372,2373],{"x":49,"y":691,"style":150},"read with bytes, write in place, record progress last",[153,2375,2377],{"id":2376},"throttling-and-concurrency","Throttling and concurrency",[10,2379,2380,2381,2384,2385,2387,2388,1960,2391,2393,2394,2397],{},"The Excel APIs in Graph do not publish simple request-per-minute limits, because the cost of a call depends heavily on the workbook. Two practices keep jobs\nout of trouble. First, honour ",[233,2382,2383],{},"Retry-After"," on ",[233,2386,1963],{}," responses and retry ",[233,2389,2390],{},"503",[233,2392,1959],{}," with backoff. Second, keep requests to any one workbook ",[212,2395,2396],{},"sequential",":\nMicrosoft's guidance is explicit that concurrent writes to the same workbook tend to cause throttling, timeouts and merge conflicts rather than speed.",[226,2399,2401],{"className":228,"code":2400,"language":230,"meta":231,"style":231},"import time\n\ndef with_retry(send, attempts: int = 5):\n    for attempt in range(1, attempts + 1):\n        response = send()\n        if response.status_code in (429, 503, 504) and attempt \u003C attempts:\n            time.sleep(float(response.headers.get(\"Retry-After\", 2 ** attempt)))\n            continue\n        response.raise_for_status()\n        return response\n\nok = with_retry(lambda: http.get(f\"{BOOK}\u002Fworksheets\", timeout=60))\nprint(ok.status_code)\n",[233,2402,2403,2410,2414,2435,2462,2472,2508,2532,2537,2542,2550,2554,2594],{"__ignoreMap":231},[236,2404,2405,2407],{"class":61,"line":238},[236,2406,242],{"class":241},[236,2408,2409],{"class":245}," time\n",[236,2411,2412],{"class":61,"line":249},[236,2413,261],{"emptyLinePlaceholder":260},[236,2415,2416,2418,2421,2424,2427,2429,2432],{"class":61,"line":257},[236,2417,308],{"class":241},[236,2419,2420],{"class":311}," with_retry",[236,2422,2423],{"class":245},"(send, attempts: ",[236,2425,2426],{"class":267},"int",[236,2428,271],{"class":241},[236,2430,2431],{"class":267}," 5",[236,2433,2434],{"class":245},"):\n",[236,2436,2437,2440,2443,2445,2448,2450,2452,2455,2457,2460],{"class":61,"line":264},[236,2438,2439],{"class":241},"    for",[236,2441,2442],{"class":245}," attempt ",[236,2444,1312],{"class":241},[236,2446,2447],{"class":267}," range",[236,2449,530],{"class":245},[236,2451,634],{"class":267},[236,2453,2454],{"class":245},", attempts ",[236,2456,1439],{"class":241},[236,2458,2459],{"class":267}," 1",[236,2461,2434],{"class":245},[236,2463,2464,2467,2469],{"class":61,"line":300},[236,2465,2466],{"class":245},"        response ",[236,2468,334],{"class":241},[236,2470,2471],{"class":245}," send()\n",[236,2473,2474,2477,2480,2482,2485,2487,2489,2491,2493,2495,2497,2500,2502,2505],{"class":61,"line":305},[236,2475,2476],{"class":241},"        if",[236,2478,2479],{"class":245}," response.status_code ",[236,2481,1312],{"class":241},[236,2483,2484],{"class":245}," (",[236,2486,1963],{"class":267},[236,2488,446],{"class":245},[236,2490,2390],{"class":267},[236,2492,446],{"class":245},[236,2494,1959],{"class":267},[236,2496,1456],{"class":245},[236,2498,2499],{"class":241},"and",[236,2501,2442],{"class":245},[236,2503,2504],{"class":241},"\u003C",[236,2506,2507],{"class":245}," attempts:\n",[236,2509,2510,2513,2516,2519,2522,2524,2526,2529],{"class":61,"line":328},[236,2511,2512],{"class":245},"            time.sleep(",[236,2514,2515],{"class":267},"float",[236,2517,2518],{"class":245},"(response.headers.get(",[236,2520,2521],{"class":277},"\"Retry-After\"",[236,2523,446],{"class":245},[236,2525,654],{"class":267},[236,2527,2528],{"class":241}," **",[236,2530,2531],{"class":245}," attempt)))\n",[236,2533,2534],{"class":61,"line":357},[236,2535,2536],{"class":241},"            continue\n",[236,2538,2539],{"class":61,"line":373},[236,2540,2541],{"class":245},"        response.raise_for_status()\n",[236,2543,2544,2547],{"class":61,"line":402},[236,2545,2546],{"class":241},"        return",[236,2548,2549],{"class":245}," response\n",[236,2551,2552],{"class":61,"line":407},[236,2553,261],{"emptyLinePlaceholder":260},[236,2555,2556,2559,2561,2564,2567,2570,2572,2574,2576,2578,2580,2583,2585,2587,2589,2591],{"class":61,"line":426},[236,2557,2558],{"class":245},"ok ",[236,2560,334],{"class":241},[236,2562,2563],{"class":245}," with_retry(",[236,2565,2566],{"class":241},"lambda",[236,2568,2569],{"class":245},": http.get(",[236,2571,965],{"class":241},[236,2573,968],{"class":277},[236,2575,282],{"class":281},[236,2577,1107],{"class":267},[236,2579,294],{"class":281},[236,2581,2582],{"class":277},"\u002Fworksheets\"",[236,2584,446],{"class":245},[236,2586,982],{"class":346},[236,2588,334],{"class":241},[236,2590,987],{"class":267},[236,2592,2593],{"class":245},"))\n",[236,2595,2596,2598],{"class":61,"line":457},[236,2597,559],{"class":267},[236,2599,2600],{"class":245},"(ok.status_code)\n",[10,2602,2603,2604,2607],{},"Parallelism is still useful across ",[221,2605,2606],{},"different"," workbooks — ten branch files can be processed by ten workers — but within one file, one request at a time is both\nthe polite and the fastest option.",[153,2609,2611],{"id":2610},"logging-what-a-job-changed","Logging what a job changed",[10,2613,2614,2615,2618,2619,2622,2623,2626],{},"A job that edits shared workbooks needs a record of what it did, because the first question after an unexpected number appears is always \"did the script\nwrite that?\". Graph's version history on the file shows ",[221,2616,2617],{},"that"," something changed; your own log should show ",[221,2620,2621],{},"what",". Record the workbook, the operation, the\nrange or table, the number of rows, the session mode and the Graph ",[233,2624,2625],{},"request-id"," response header, which is what Microsoft support asks for when a call behaves\nstrangely.",[226,2628,2630],{"className":228,"code":2629,"language":230,"meta":231,"style":231},"import json\nimport logging\nfrom datetime import datetime, timezone\n\nlogging.basicConfig(level=logging.INFO, format=\"%(message)s\")\nlog = logging.getLogger(\"graph-excel\")\n\ndef audited(operation: str, target: str, response, rows: int = 0) -> None:\n    log.info(json.dumps({\n        \"at\": datetime.now(timezone.utc).isoformat(timespec=\"seconds\"),\n        \"operation\": operation,\n        \"workbook\": item[\"name\"],\n        \"target\": target,\n        \"rows\": rows,\n        \"status\": response.status_code,\n        \"request_id\": response.headers.get(\"request-id\"),\n        \"session\": \"workbook-session-id\" in http.headers,\n    }))\n\naudited(\"rows\u002Fadd\", \"Orders\", added, rows=len(rows))\n",[233,2631,2632,2639,2646,2658,2662,2694,2709,2713,2747,2752,2771,2779,2792,2800,2808,2816,2829,2844,2849,2853],{"__ignoreMap":231},[236,2633,2634,2636],{"class":61,"line":238},[236,2635,242],{"class":241},[236,2637,2638],{"class":245}," json\n",[236,2640,2641,2643],{"class":61,"line":249},[236,2642,242],{"class":241},[236,2644,2645],{"class":245}," logging\n",[236,2647,2648,2650,2653,2655],{"class":61,"line":257},[236,2649,889],{"class":241},[236,2651,2652],{"class":245}," datetime ",[236,2654,242],{"class":241},[236,2656,2657],{"class":245}," datetime, timezone\n",[236,2659,2660],{"class":61,"line":264},[236,2661,261],{"emptyLinePlaceholder":260},[236,2663,2664,2667,2670,2672,2675,2678,2680,2683,2685,2687,2690,2692],{"class":61,"line":300},[236,2665,2666],{"class":245},"logging.basicConfig(",[236,2668,2669],{"class":346},"level",[236,2671,334],{"class":241},[236,2673,2674],{"class":245},"logging.",[236,2676,2677],{"class":267},"INFO",[236,2679,446],{"class":245},[236,2681,2682],{"class":346},"format",[236,2684,334],{"class":241},[236,2686,968],{"class":277},[236,2688,2689],{"class":281},"%(message)s",[236,2691,968],{"class":277},[236,2693,536],{"class":245},[236,2695,2696,2699,2701,2704,2707],{"class":61,"line":305},[236,2697,2698],{"class":245},"log ",[236,2700,334],{"class":241},[236,2702,2703],{"class":245}," logging.getLogger(",[236,2705,2706],{"class":277},"\"graph-excel\"",[236,2708,536],{"class":245},[236,2710,2711],{"class":61,"line":328},[236,2712,261],{"emptyLinePlaceholder":260},[236,2714,2715,2717,2720,2723,2725,2728,2730,2733,2735,2737,2740,2743,2745],{"class":61,"line":357},[236,2716,308],{"class":241},[236,2718,2719],{"class":311}," audited",[236,2721,2722],{"class":245},"(operation: ",[236,2724,318],{"class":267},[236,2726,2727],{"class":245},", target: ",[236,2729,318],{"class":267},[236,2731,2732],{"class":245},", response, rows: ",[236,2734,2426],{"class":267},[236,2736,271],{"class":241},[236,2738,2739],{"class":267}," 0",[236,2741,2742],{"class":245},") -> ",[236,2744,1299],{"class":267},[236,2746,2064],{"class":245},[236,2748,2749],{"class":61,"line":373},[236,2750,2751],{"class":245},"    log.info(json.dumps({\n",[236,2753,2754,2757,2760,2763,2765,2768],{"class":61,"line":402},[236,2755,2756],{"class":277},"        \"at\"",[236,2758,2759],{"class":245},": datetime.now(timezone.utc).isoformat(",[236,2761,2762],{"class":346},"timespec",[236,2764,334],{"class":241},[236,2766,2767],{"class":277},"\"seconds\"",[236,2769,2770],{"class":245},"),\n",[236,2772,2773,2776],{"class":61,"line":407},[236,2774,2775],{"class":277},"        \"operation\"",[236,2777,2778],{"class":245},": operation,\n",[236,2780,2781,2784,2787,2789],{"class":61,"line":426},[236,2782,2783],{"class":277},"        \"workbook\"",[236,2785,2786],{"class":245},": item[",[236,2788,1155],{"class":277},[236,2790,2791],{"class":245},"],\n",[236,2793,2794,2797],{"class":61,"line":457},[236,2795,2796],{"class":277},"        \"target\"",[236,2798,2799],{"class":245},": target,\n",[236,2801,2802,2805],{"class":61,"line":468},[236,2803,2804],{"class":277},"        \"rows\"",[236,2806,2807],{"class":245},": rows,\n",[236,2809,2810,2813],{"class":61,"line":509},[236,2811,2812],{"class":277},"        \"status\"",[236,2814,2815],{"class":245},": response.status_code,\n",[236,2817,2818,2821,2824,2827],{"class":61,"line":521},[236,2819,2820],{"class":277},"        \"request_id\"",[236,2822,2823],{"class":245},": response.headers.get(",[236,2825,2826],{"class":277},"\"request-id\"",[236,2828,2770],{"class":245},[236,2830,2831,2834,2836,2838,2841],{"class":61,"line":539},[236,2832,2833],{"class":277},"        \"session\"",[236,2835,1928],{"class":245},[236,2837,2045],{"class":277},[236,2839,2840],{"class":241}," in",[236,2842,2843],{"class":245}," http.headers,\n",[236,2845,2846],{"class":61,"line":551},[236,2847,2848],{"class":245},"    }))\n",[236,2850,2851],{"class":61,"line":556},[236,2852,261],{"emptyLinePlaceholder":260},[236,2854,2856,2859,2862,2864,2867,2870,2873,2875,2877],{"class":61,"line":2855},20,[236,2857,2858],{"class":245},"audited(",[236,2860,2861],{"class":277},"\"rows\u002Fadd\"",[236,2863,446],{"class":245},[236,2865,2866],{"class":277},"\"Orders\"",[236,2868,2869],{"class":245},", added, ",[236,2871,2872],{"class":346},"rows",[236,2874,334],{"class":241},[236,2876,1511],{"class":267},[236,2878,2879],{"class":245},"(rows))\n",[10,2881,2882,2883,203],{},"One JSON object per line is easy to search and easy to ship to whatever log store the rest of your jobs use. Pair it with the eTag you recorded before and after\nthe run, and an unexpected change can be traced to a specific run, operation and request in a few minutes. The same structured-logging approach for whole report jobs\nis described in ",[14,2884,2886],{"href":2885},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File",[153,2888,2890],{"id":2889},"when-not-to-use-graph-for-excel-work","When not to use Graph for Excel work",[10,2892,2893,2894,203],{},"Graph is the right tool when the workbook's home is Microsoft 365 and people keep using it there. It is the wrong tool when a script is really building a report\nfrom scratch — that is faster and more controllable with xlsxwriter or openpyxl locally, followed by a single upload. It is also a poor fit for high-volume data:\nan Excel table in SharePoint is comfortable with thousands of rows and awkward with millions, and a database behind the workbook is the durable design at that\nscale. And when a step needs desktop-only features — VBA macros, some data connection types — the options are the desktop application through COM or xlwings, as in\n",[14,2895,2897],{"href":2896},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32",[153,2899,2901],{"id":2900},"common-failures-at-a-glance","Common failures at a glance",[766,2903,2904,2917],{},[769,2905,2906],{},[772,2907,2908,2911,2914],{},[775,2909,2910],{},"Symptom",[775,2912,2913],{},"Likely cause",[775,2915,2916],{},"Where to look",[785,2918,2919,2930,2947,2963,2977,2988],{},[772,2920,2921,2924,2927],{},[790,2922,2923],{},"Workbook calls fail though file downloads work",[790,2925,2926],{},"App-only token on a delegated-only endpoint",[790,2928,2929],{},"Authentication guide",[772,2931,2932,2938,2944],{},[790,2933,2934,2937],{},[233,2935,2936],{},"403"," on a SharePoint file",[790,2939,2940,2941,2943],{},"Consent missing, ",[233,2942,828],{}," without a site grant, or no access for the account",[790,2945,2946],{},"Authentication and reading guides",[772,2948,2949,2952,2960],{},[790,2950,2951],{},"Cleared cells keep old values",[790,2953,2954,2956,2957],{},[233,2955,1603],{}," sent instead of ",[233,2958,2959],{},"\"\"",[790,2961,2962],{},"Ranges guide",[772,2964,2965,2968,2974],{},[790,2966,2967],{},"Duplicate rows after a rerun",[790,2969,2970,2971],{},"No key check before ",[233,2972,2973],{},"rows\u002Fadd",[790,2975,2976],{},"Tables guide",[772,2978,2979,2982,2985],{},[790,2980,2981],{},"Outputs do not change after writing inputs",[790,2983,2984],{},"Manual calculation mode",[790,2986,2987],{},"Sessions guide",[772,2989,2990,2996,2999],{},[790,2991,2992,2993,2995],{},"Frequent ",[233,2994,1963],{}," responses",[790,2997,2998],{},"Parallel or single-row requests",[790,3000,3001],{},"Tables and sessions guides",[153,3003,3005],{"id":3004},"key-takeaways","Key takeaways",[158,3007,3008,3011,3014,3023,3035,3041,3047],{},[161,3009,3010],{},"File endpoints (download, upload) accept app-only tokens; the workbook API (ranges, tables, sessions, calculate) needs delegated tokens.",[161,3012,3013],{},"A delegated job can run on a schedule with a device-code sign-in once and a protected MSAL token cache.",[161,3015,3016,3017,3019,3020,3022],{},"Download with ",[233,3018,1184],{}," into ",[233,3021,1188],{}," to read whole workbooks into pandas; use eTags to skip unchanged files.",[161,3024,3025,3026,3028,3029,3031,3032,3034],{},"Edit ranges in place with ",[233,3027,1345],{},": exact dimensions, ",[233,3030,2959],{}," for blanks, ",[233,3033,1618],{}," for NumPy values, serial numbers for dates.",[161,3036,3037,3038,3040],{},"Append to shared tables with ",[233,3039,2973],{}," in batches, aligned to the header and de-duplicated by key.",[161,3042,3043,3044,3046],{},"Use sessions for multi-call jobs; non-persistent sessions plus ",[233,3045,2353],{}," turn a shared model into a calculation service.",[161,3048,3049,3050,203],{},"Keep requests to one workbook sequential and honour ",[233,3051,2383],{},[153,3053,3055],{"id":3054},"frequently-asked-questions","Frequently asked questions",[10,3057,3058,3061],{},[212,3059,3060],{},"Do I need Excel installed to use the Graph workbook API?","\nNo. The workbook API runs on Excel's online service in Microsoft 365. Your script only needs HTTP access, a token and the workbook's location.",[10,3063,3064,3067],{},[212,3065,3066],{},"Does the workbook API work with .xls or .csv files?","\nNo. It works with .xlsx workbooks. Convert older formats first, or read them by downloading the file content and using pandas with the appropriate engine.",[10,3069,3070,3073],{},[212,3071,3072],{},"Can a scheduled job use the workbook API without a user account?","\nNot with application permissions, which the workbook endpoints do not support. Use a dedicated account with a cached delegated sign-in, or download, modify and upload the file with an app-only token when nobody edits it concurrently.",[10,3075,3076,3079],{},[212,3077,3078],{},"Is the Graph SDK for Python required?","\nNo. The endpoints are plain REST calls, and requests with MSAL is enough, as used throughout this section. The msgraph-sdk package works too if you prefer typed request builders.",[10,3081,3082,3085],{},[212,3083,3084],{},"Will people with the workbook open see changes made through Graph?","\nYes. Range updates and table appends through the workbook API behave like co-authoring edits, so open copies in Excel for the web update as the job runs.",[10,3087,3088,3091],{},[212,3089,3090],{},"How do I test a Graph job without touching production workbooks?","\nPoint the job at a copy of the workbook in a test library by keeping drive and item IDs in configuration, and use non-persistent sessions for dry runs. For unit tests, replace the HTTP session with a fake that returns recorded responses, so the transformation logic is tested without any network calls.",[153,3093,3095],{"id":3094},"related","Related",[158,3097,3098,3104,3108,3112,3116,3120,3124,3131,3138],{},[161,3099,3100,3101,3103],{},"Up one level: ",[14,3102,17],{"href":16}," — scheduling, delivery and packaging for Excel reports.",[161,3105,3106],{},[14,3107,166],{"href":165},[161,3109,3110],{},[14,3111,173],{"href":172},[161,3113,3114],{},[14,3115,180],{"href":179},[161,3117,3118],{},[14,3119,187],{"href":186},[161,3121,3122],{},[14,3123,194],{"href":193},[161,3125,3126,3130],{},[14,3127,3129],{"href":3128},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage"," — uploading finished reports to SharePoint, S3 and Drive.",[161,3132,3133,3137],{},[14,3134,3136],{"href":3135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api\u002F","Send an Excel Report with the Microsoft Graph API"," — mail delivery through the same platform.",[161,3139,3140,3144],{},[14,3141,3143],{"href":3142},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-python-in-excel\u002F","Using Python in Excel"," — Python inside the workbook rather than calling into it.",[10,3146,3147],{},"More in this section:",[158,3149,3150,3156,3162,3168,3174,3180,3186],{},[161,3151,3152],{},[14,3153,3155],{"href":3154},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph\u002F","Add a Chart to an Excel Online Workbook with Microsoft Graph",[161,3157,3158],{},[14,3159,3161],{"href":3160},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph\u002F","Add, Rename and Delete Worksheets with Microsoft Graph",[161,3163,3164],{},[14,3165,3167],{"href":3166},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcopy-a-template-workbook-in-sharepoint-with-microsoft-graph\u002F","Copy a Template Workbook in SharePoint with Microsoft Graph",[161,3169,3170],{},[14,3171,3173],{"href":3172},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcreate-a-new-workbook-in-onedrive-with-microsoft-graph\u002F","Create a New Workbook in OneDrive with Microsoft Graph",[161,3175,3176],{},[14,3177,3179],{"href":3178},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive\u002F","Run a Report When an Excel File Changes in OneDrive",[161,3181,3182],{},[14,3183,3185],{"href":3184},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fshare-an-excel-file-in-onedrive-with-a-link-using-graph\u002F","Share an Excel File in OneDrive with a Link Using Graph",[161,3187,3188],{},[14,3189,3191],{"href":3190},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fsort-and-filter-an-excel-online-table-with-microsoft-graph\u002F","Sort and Filter an Excel Online Table with Microsoft Graph",[3193,3194,3195],"style",{},"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 .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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);}",{"title":231,"searchDepth":249,"depth":249,"links":3197},[3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212],{"id":155,"depth":249,"text":156},{"id":206,"depth":249,"text":207},{"id":760,"depth":249,"text":761},{"id":860,"depth":249,"text":861},{"id":1177,"depth":249,"text":1178},{"id":1338,"depth":249,"text":1339},{"id":1712,"depth":249,"text":1713},{"id":1969,"depth":249,"text":1970},{"id":2376,"depth":249,"text":2377},{"id":2610,"depth":249,"text":2611},{"id":2889,"depth":249,"text":2890},{"id":2900,"depth":249,"text":2901},{"id":3004,"depth":249,"text":3005},{"id":3054,"depth":249,"text":3055},{"id":3094,"depth":249,"text":3095},"2026-09-17","Work with workbooks that live in SharePoint and OneDrive from Python: the right token for each call, reading files into pandas, editing ranges and tables in place, and recalculating models with sessions.","md",[3217,3219,3221,3223,3225,3227],{"q":3060,"a":3218},"No. The workbook API runs on Excel's online service in Microsoft 365. Your script only needs HTTP access, a token and the workbook's location.",{"q":3066,"a":3220},"No. It works with .xlsx workbooks. Convert older formats first, or read them by downloading the file content and using pandas with the appropriate engine.",{"q":3072,"a":3222},"Not with application permissions, which the workbook endpoints do not support. Use a dedicated account with a cached delegated sign-in, or download, modify and upload the file with an app-only token when nobody edits it concurrently.",{"q":3078,"a":3224},"No. The endpoints are plain REST calls, and requests with MSAL is enough, as used throughout this section. The msgraph-sdk package works too if you prefer typed request builders.",{"q":3084,"a":3226},"Yes. Range updates and table appends through the workbook API behave like co-authoring edits, so open copies in Excel for the web update as the job runs.",{"q":3090,"a":3228},"Point the job at a copy of the workbook in a test library by keeping drive and item IDs in configuration, and use non-persistent sessions for dry runs. For unit tests, replace the HTTP session with a fake that returns recorded responses, so the transformation logic is tested without any network calls.","automating excel online with microsoft graph",null,{"breadcrumb":3232},[3233,3236,3237],{"name":3234,"item":3235},"Home","\u002F",{"name":17,"item":16},{"name":5,"item":3238},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002F","\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph",{"title":3241,"description":3242},"Automating Excel Online with Microsoft Graph and Python","A practical guide to the Microsoft Graph Excel APIs from Python: delegated versus app-only tokens, downloading SharePoint workbooks, range updates, table appends, sessions, calculation and throttling.","automating-excel-online-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Findex",[3246],"excel-online","guide","fn0DjmXEJgWfCFITY5HKHAPpiBY4DNI5miwIxMkQoIY",[3250,3254],{"title":3251,"path":3252,"stem":3253,"children":-1},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows","automating-reporting-workflows\u002Findex",{"title":3155,"path":3255,"stem":3256,"children":-1},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph\u002Findex",[3258],{"path":3252,"title":3251},[3260,3264],{"path":3261,"title":3262,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","Call Excel Worksheet Functions Through Microsoft Graph","how-to",{"path":3265,"title":3266,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",[3268,3269,3271,3273,3275,3277],{"path":3255,"title":3155,"kind":3263},{"path":3270,"title":3161,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph",{"path":3272,"title":187,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph",{"path":3274,"title":166,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation",{"path":3276,"title":3167,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcopy-a-template-workbook-in-sharepoint-with-microsoft-graph",{"path":3278,"title":3173,"kind":3263},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcreate-a-new-workbook-in-onedrive-with-microsoft-graph",1790972053407]