[{"data":1,"prerenderedAt":2225},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates":2193,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates":2201,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates":2205,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates":2206},{"id":4,"title":5,"body":6,"dateModified":2160,"datePublished":2160,"description":2161,"extension":2162,"faq":2163,"keyword":2172,"kind":2173,"meta":2174,"navigation":277,"path":2182,"seo":2183,"slug":2186,"stem":2187,"topics":2188,"type":2191,"__hash__":2192},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates\u002Findex.md","Backfill Excel Reports for Past Dates with Python",{"type":7,"value":8,"toc":2146},"minimark",[9,19,175,180,208,215,219,222,696,709,713,735,1032,1039,1083,1086,1090,1093,1281,1300,1304,1311,1574,1581,1658,1662,1667,1859,1866,1870,1878,1993,1997,2044,2048,2059,2063,2081,2085,2091,2097,2103,2109,2113,2142],[10,11,12,13,18],"p",{},"Sooner or later a daily report needs running for dates in the past: the job was down for a week, a bug in last month's calculation has been fixed, or a new report needs a year of history before anyone trusts it. If the script reads the clock to decide what to report, backfilling means editing the code or faking the system date. Built the right way from the start, it is a loop over dates. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", turns a daily report into one that can be run for any date or range, safely and repeatably.",[20,21,29,30,29,34,29,38,29,46,29,56,29,67,29,74,29,79,29,85,29,90,29,93,29,99,29,103,29,106,29,110,29,113,29,116,29,122,29,126,29,129,29,133,29,136,29,139,29,145,29,149,29,152,29,156,29,159,29,162,29,168,29,172],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"4 0 752 125","img",[25,26],"bf-flow-t","bf-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"A backfill run from command line to summary",[35,36,37],"desc",{"id":26},"Five steps. Parse the date range from the command line. Build the list of business days in it. For each date, skip it if the output already exists, unless forced. Generate the report for that date, catching failures. Print a summary of what was made, skipped and failed, which can be rerun for the failures only.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"4","0","752","125","#ffffff",[39,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"20.0","16.0","115.2","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,60],"g",{"transform":59},"translate(65.6,29.0) scale(1.000)",[61,62],"path",{"d":63,"fill":64,"stroke":65,"style":66},"M4 5h16v14H4z M7 9l3 3-3 3 M12 15h5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[68,69,73],"text",{"x":70,"y":71,"style":72},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Parse range",[68,75,78],{"x":70,"y":76,"style":77},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","--from --to",[80,81],"line",{"x1":82,"y1":83,"x2":84,"y2":83,"stroke":54,"style":55},"140.2","62.5","162.2",[86,87],"polygon",{"points":88,"fill":89},"166.2,62.5 157.2,67.5 157.2,57.5","#5b5cf0",[39,91],{"x":92,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"171.2",[57,94,96],{"transform":95},"translate(216.8,29.0) scale(1.000)",[61,97],{"d":98,"fill":64,"stroke":65,"style":66},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4",[68,100,102],{"x":101,"y":71,"style":72},"228.8","List dates",[68,104,105],{"x":101,"y":76,"style":77},"business days",[80,107],{"x1":108,"y1":83,"x2":109,"y2":83,"stroke":54,"style":55},"291.4","313.4",[86,111],{"points":112,"fill":89},"317.4,62.5 308.4,67.5 308.4,57.5",[39,114],{"x":115,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"322.4",[57,117,119],{"transform":118},"translate(368.0,29.0) scale(1.000)",[61,120],{"d":121,"fill":64,"stroke":65,"style":66},"M3 5h18l-7 8v6l-4 2v-8z",[68,123,125],{"x":124,"y":71,"style":72},"380.0","Skip existing",[68,127,128],{"x":124,"y":76,"style":77},"unless --force",[80,130],{"x1":131,"y1":83,"x2":132,"y2":83,"stroke":54,"style":55},"442.6","464.6",[86,134],{"points":135,"fill":89},"468.6,62.5 459.6,67.5 459.6,57.5",[39,137],{"x":138,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"473.6",[57,140,142],{"transform":141},"translate(519.2,29.0) scale(1.000)",[61,143],{"d":144,"fill":64,"stroke":65,"style":66},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1",[68,146,148],{"x":147,"y":71,"style":72},"531.2","Generate",[68,150,151],{"x":147,"y":76,"style":77},"catch failures",[80,153],{"x1":154,"y1":83,"x2":155,"y2":83,"stroke":54,"style":55},"593.8","615.8",[86,157],{"points":158,"fill":89},"619.8,62.5 610.8,67.5 610.8,57.5",[39,160],{"x":161,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"624.8",[57,163,165],{"transform":164},"translate(670.4,29.0) scale(1.000)",[61,166],{"d":167,"fill":64,"stroke":65,"style":66},"M5 12.5l4.5 4.5L19 7",[68,169,171],{"x":170,"y":71,"style":72},"682.4","Summary",[68,173,174],{"x":170,"y":76,"style":77},"rerun failures",[176,177,179],"h2",{"id":178},"prerequisites","Prerequisites",[181,182,187],"pre",{"className":183,"code":184,"language":185,"meta":186,"style":186},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[188,189,190],"code",{"__ignoreMap":186},[191,192,194,198,202,205],"span",{"class":80,"line":193},1,[191,195,197],{"class":196},"sMTad","pip",[191,199,201],{"class":200},"srMev"," install",[191,203,204],{"class":200}," pandas",[191,206,207],{"class":200}," openpyxl\n",[10,209,210,211,214],{},"The ",[188,212,213],{},"inputs"," folder has one sales file per business day of September 2026, named by date — except the 17th, which is missing, as happens when an upstream export fails.",[176,216,218],{"id":217},"step-1-report-for-an-as-of-date-never-now","Step 1 — Report for an as-of date, never \"now\"",[10,220,221],{},"The single change that makes backfilling possible: the report function takes the date it reports on as a parameter, and every date-dependent choice — which input to read, what to filter, what to call the output, what the title says — comes from that parameter:",[181,223,227],{"className":224,"code":225,"language":226,"meta":186,"style":186},"language-python shiki shiki-themes github-light github-dark-high-contrast","from datetime import date\nfrom pathlib import Path\nimport pandas as pd\n\nOUT = Path(\"reports\"); OUT.mkdir(exist_ok=True)\n\ndef output_path(as_of):\n    return OUT \u002F f\"daily_sales_{as_of:%Y-%m-%d}.xlsx\"\n\ndef generate_report(as_of: date) -> Path:\n    source = Path(\"inputs\") \u002F f\"sales_{as_of:%Y-%m-%d}.xlsx\"\n    if not source.exists():\n        raise FileNotFoundError(f\"no input for {as_of}: {source}\")\n    df = pd.read_excel(source)\n    summary = df.assign(Share=df[\"Revenue\"] \u002F df[\"Revenue\"].sum())\n    target = output_path(as_of)\n    with pd.ExcelWriter(target, engine=\"openpyxl\") as writer:\n        summary.to_excel(writer, sheet_name=\"Sales\", index=False, startrow=2)\n        writer.sheets[\"Sales\"][\"A1\"] = f\"Daily sales — {as_of:%d %B %Y}\"\n    return target\n\nprint(generate_report(date(2026, 9, 3)).name)\n","python",[188,228,229,245,258,272,279,316,321,334,382,387,398,442,454,494,505,540,551,575,612,658,666,671],{"__ignoreMap":186},[191,230,231,235,239,242],{"class":80,"line":193},[191,232,234],{"class":233},"s-kum","from",[191,236,238],{"class":237},"skGVy"," datetime ",[191,240,241],{"class":233},"import",[191,243,244],{"class":237}," date\n",[191,246,248,250,253,255],{"class":80,"line":247},2,[191,249,234],{"class":233},[191,251,252],{"class":237}," pathlib ",[191,254,241],{"class":233},[191,256,257],{"class":237}," Path\n",[191,259,261,263,266,269],{"class":80,"line":260},3,[191,262,241],{"class":233},[191,264,265],{"class":237}," pandas ",[191,267,268],{"class":233},"as",[191,270,271],{"class":237}," pd\n",[191,273,275],{"class":80,"line":274},4,[191,276,278],{"emptyLinePlaceholder":277},true,"\n",[191,280,282,286,289,292,295,298,300,303,307,310,313],{"class":80,"line":281},5,[191,283,285],{"class":284},"sP0c6","OUT",[191,287,288],{"class":233}," =",[191,290,291],{"class":237}," Path(",[191,293,294],{"class":200},"\"reports\"",[191,296,297],{"class":237},"); ",[191,299,285],{"class":284},[191,301,302],{"class":237},".mkdir(",[191,304,306],{"class":305},"sa561","exist_ok",[191,308,309],{"class":233},"=",[191,311,312],{"class":284},"True",[191,314,315],{"class":237},")\n",[191,317,319],{"class":80,"line":318},6,[191,320,278],{"emptyLinePlaceholder":277},[191,322,324,327,331],{"class":80,"line":323},7,[191,325,326],{"class":233},"def",[191,328,330],{"class":329},"s_Opv"," output_path",[191,332,333],{"class":237},"(as_of):\n",[191,335,337,340,343,346,349,352,356,359,362,365,368,371,373,376,379],{"class":80,"line":336},8,[191,338,339],{"class":233},"    return",[191,341,342],{"class":284}," OUT",[191,344,345],{"class":233}," \u002F",[191,347,348],{"class":233}," f",[191,350,351],{"class":200},"\"daily_sales_",[191,353,355],{"class":354},"sSjpA","{",[191,357,358],{"class":237},"as_of:",[191,360,361],{"class":233},"%",[191,363,364],{"class":237},"Y",[191,366,367],{"class":233},"-%",[191,369,370],{"class":237},"m",[191,372,367],{"class":233},[191,374,375],{"class":237},"d",[191,377,378],{"class":354},"}",[191,380,381],{"class":200},".xlsx\"\n",[191,383,385],{"class":80,"line":384},9,[191,386,278],{"emptyLinePlaceholder":277},[191,388,390,392,395],{"class":80,"line":389},10,[191,391,326],{"class":233},[191,393,394],{"class":329}," generate_report",[191,396,397],{"class":237},"(as_of: date) -> Path:\n",[191,399,401,404,406,408,411,414,417,419,422,424,426,428,430,432,434,436,438,440],{"class":80,"line":400},11,[191,402,403],{"class":237},"    source ",[191,405,309],{"class":233},[191,407,291],{"class":237},[191,409,410],{"class":200},"\"inputs\"",[191,412,413],{"class":237},") ",[191,415,416],{"class":233},"\u002F",[191,418,348],{"class":233},[191,420,421],{"class":200},"\"sales_",[191,423,355],{"class":354},[191,425,358],{"class":237},[191,427,361],{"class":233},[191,429,364],{"class":237},[191,431,367],{"class":233},[191,433,370],{"class":237},[191,435,367],{"class":233},[191,437,375],{"class":237},[191,439,378],{"class":354},[191,441,381],{"class":200},[191,443,445,448,451],{"class":80,"line":444},12,[191,446,447],{"class":233},"    if",[191,449,450],{"class":233}," not",[191,452,453],{"class":237}," source.exists():\n",[191,455,457,460,463,466,469,472,474,477,479,482,484,487,489,492],{"class":80,"line":456},13,[191,458,459],{"class":233},"        raise",[191,461,462],{"class":284}," FileNotFoundError",[191,464,465],{"class":237},"(",[191,467,468],{"class":233},"f",[191,470,471],{"class":200},"\"no input for ",[191,473,355],{"class":354},[191,475,476],{"class":237},"as_of",[191,478,378],{"class":354},[191,480,481],{"class":200},": ",[191,483,355],{"class":354},[191,485,486],{"class":237},"source",[191,488,378],{"class":354},[191,490,491],{"class":200},"\"",[191,493,315],{"class":237},[191,495,497,500,502],{"class":80,"line":496},14,[191,498,499],{"class":237},"    df ",[191,501,309],{"class":233},[191,503,504],{"class":237}," pd.read_excel(source)\n",[191,506,508,511,513,516,519,521,524,527,530,532,535,537],{"class":80,"line":507},15,[191,509,510],{"class":237},"    summary ",[191,512,309],{"class":233},[191,514,515],{"class":237}," df.assign(",[191,517,518],{"class":305},"Share",[191,520,309],{"class":233},[191,522,523],{"class":237},"df[",[191,525,526],{"class":200},"\"Revenue\"",[191,528,529],{"class":237},"] ",[191,531,416],{"class":233},[191,533,534],{"class":237}," df[",[191,536,526],{"class":200},[191,538,539],{"class":237},"].sum())\n",[191,541,543,546,548],{"class":80,"line":542},16,[191,544,545],{"class":237},"    target ",[191,547,309],{"class":233},[191,549,550],{"class":237}," output_path(as_of)\n",[191,552,554,557,560,563,565,568,570,572],{"class":80,"line":553},17,[191,555,556],{"class":233},"    with",[191,558,559],{"class":237}," pd.ExcelWriter(target, ",[191,561,562],{"class":305},"engine",[191,564,309],{"class":233},[191,566,567],{"class":200},"\"openpyxl\"",[191,569,413],{"class":237},[191,571,268],{"class":233},[191,573,574],{"class":237}," writer:\n",[191,576,578,581,584,586,589,592,595,597,600,602,605,607,610],{"class":80,"line":577},18,[191,579,580],{"class":237},"        summary.to_excel(writer, ",[191,582,583],{"class":305},"sheet_name",[191,585,309],{"class":233},[191,587,588],{"class":200},"\"Sales\"",[191,590,591],{"class":237},", ",[191,593,594],{"class":305},"index",[191,596,309],{"class":233},[191,598,599],{"class":284},"False",[191,601,591],{"class":237},[191,603,604],{"class":305},"startrow",[191,606,309],{"class":233},[191,608,609],{"class":284},"2",[191,611,315],{"class":237},[191,613,615,618,620,623,626,628,630,632,635,637,639,641,644,646,649,651,653,655],{"class":80,"line":614},19,[191,616,617],{"class":237},"        writer.sheets[",[191,619,588],{"class":200},[191,621,622],{"class":237},"][",[191,624,625],{"class":200},"\"A1\"",[191,627,529],{"class":237},[191,629,309],{"class":233},[191,631,348],{"class":233},[191,633,634],{"class":200},"\"Daily sales — ",[191,636,355],{"class":354},[191,638,358],{"class":237},[191,640,361],{"class":233},[191,642,643],{"class":237},"d ",[191,645,361],{"class":233},[191,647,648],{"class":237},"B ",[191,650,361],{"class":233},[191,652,364],{"class":237},[191,654,378],{"class":354},[191,656,657],{"class":200},"\"\n",[191,659,661,663],{"class":80,"line":660},20,[191,662,339],{"class":233},[191,664,665],{"class":237}," target\n",[191,667,669],{"class":80,"line":668},21,[191,670,278],{"emptyLinePlaceholder":277},[191,672,674,677,680,683,685,688,690,693],{"class":80,"line":673},22,[191,675,676],{"class":284},"print",[191,678,679],{"class":237},"(generate_report(date(",[191,681,682],{"class":284},"2026",[191,684,591],{"class":237},[191,686,687],{"class":284},"9",[191,689,591],{"class":237},[191,691,692],{"class":284},"3",[191,694,695],{"class":237},")).name)\n",[10,697,698,699,703,704,708],{},"Nothing in the function asks what day it is. The scheduled run passes today's date; a backfill passes others. Naming the output after the ",[700,701,702],"strong",{},"as-of date",", not the date it was produced, means rerunning 3 September replaces the 3 September report rather than creating a file dated today — the most common backfill bug, which ",[14,705,707],{"href":706},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Forchestrate-excel-reports-with-apache-airflow\u002F","Orchestrate Excel Reports with Apache Airflow"," calls the logical date.",[176,710,712],{"id":711},"step-2-accept-a-date-or-a-range-on-the-command-line","Step 2 — Accept a date or a range on the command line",[10,714,715,716,719,720,416,723,726,727,730,731,734],{},"One entry point serves both uses: no arguments means \"today\" for the scheduler, ",[188,717,718],{},"--date"," means one past day, and ",[188,721,722],{},"--from",[188,724,725],{},"--to"," means a range. ",[188,728,729],{},"argparse"," converts the strings to dates with ",[188,732,733],{},"date.fromisoformat",":",[181,736,738],{"className":224,"code":737,"language":226,"meta":186,"style":186},"import argparse\n\ndef parse_args(argv=None):\n    p = argparse.ArgumentParser(description=\"Daily sales report\")\n    p.add_argument(\"--date\", type=date.fromisoformat, help=\"report for one day (YYYY-MM-DD)\")\n    p.add_argument(\"--from\", dest=\"start\", type=date.fromisoformat, help=\"first day of a backfill\")\n    p.add_argument(\"--to\", dest=\"end\", type=date.fromisoformat, help=\"last day of a backfill\")\n    p.add_argument(\"--force\", action=\"store_true\", help=\"replace reports that already exist\")\n    p.add_argument(\"--send\", action=\"store_true\", help=\"deliver the reports (off by default)\")\n    args = p.parse_args(argv)\n    if (args.start is None) != (args.end is None):\n        p.error(\"--from and --to go together\")\n    return args\n\nargs = parse_args([\"--from\", \"2026-09-14\", \"--to\", \"2026-09-18\"])\nprint(args.start, args.end, args.force, args.send)\n",[188,739,740,747,751,769,789,817,851,884,912,938,948,975,985,992,996,1025],{"__ignoreMap":186},[191,741,742,744],{"class":80,"line":193},[191,743,241],{"class":233},[191,745,746],{"class":237}," argparse\n",[191,748,749],{"class":80,"line":247},[191,750,278],{"emptyLinePlaceholder":277},[191,752,753,755,758,761,763,766],{"class":80,"line":260},[191,754,326],{"class":233},[191,756,757],{"class":329}," parse_args",[191,759,760],{"class":237},"(argv",[191,762,309],{"class":233},[191,764,765],{"class":284},"None",[191,767,768],{"class":237},"):\n",[191,770,771,774,776,779,782,784,787],{"class":80,"line":274},[191,772,773],{"class":237},"    p ",[191,775,309],{"class":233},[191,777,778],{"class":237}," argparse.ArgumentParser(",[191,780,781],{"class":305},"description",[191,783,309],{"class":233},[191,785,786],{"class":200},"\"Daily sales report\"",[191,788,315],{"class":237},[191,790,791,794,797,799,802,804,807,810,812,815],{"class":80,"line":281},[191,792,793],{"class":237},"    p.add_argument(",[191,795,796],{"class":200},"\"--date\"",[191,798,591],{"class":237},[191,800,801],{"class":305},"type",[191,803,309],{"class":233},[191,805,806],{"class":237},"date.fromisoformat, ",[191,808,809],{"class":305},"help",[191,811,309],{"class":233},[191,813,814],{"class":200},"\"report for one day (YYYY-MM-DD)\"",[191,816,315],{"class":237},[191,818,819,821,824,826,829,831,834,836,838,840,842,844,846,849],{"class":80,"line":318},[191,820,793],{"class":237},[191,822,823],{"class":200},"\"--from\"",[191,825,591],{"class":237},[191,827,828],{"class":305},"dest",[191,830,309],{"class":233},[191,832,833],{"class":200},"\"start\"",[191,835,591],{"class":237},[191,837,801],{"class":305},[191,839,309],{"class":233},[191,841,806],{"class":237},[191,843,809],{"class":305},[191,845,309],{"class":233},[191,847,848],{"class":200},"\"first day of a backfill\"",[191,850,315],{"class":237},[191,852,853,855,858,860,862,864,867,869,871,873,875,877,879,882],{"class":80,"line":323},[191,854,793],{"class":237},[191,856,857],{"class":200},"\"--to\"",[191,859,591],{"class":237},[191,861,828],{"class":305},[191,863,309],{"class":233},[191,865,866],{"class":200},"\"end\"",[191,868,591],{"class":237},[191,870,801],{"class":305},[191,872,309],{"class":233},[191,874,806],{"class":237},[191,876,809],{"class":305},[191,878,309],{"class":233},[191,880,881],{"class":200},"\"last day of a backfill\"",[191,883,315],{"class":237},[191,885,886,888,891,893,896,898,901,903,905,907,910],{"class":80,"line":336},[191,887,793],{"class":237},[191,889,890],{"class":200},"\"--force\"",[191,892,591],{"class":237},[191,894,895],{"class":305},"action",[191,897,309],{"class":233},[191,899,900],{"class":200},"\"store_true\"",[191,902,591],{"class":237},[191,904,809],{"class":305},[191,906,309],{"class":233},[191,908,909],{"class":200},"\"replace reports that already exist\"",[191,911,315],{"class":237},[191,913,914,916,919,921,923,925,927,929,931,933,936],{"class":80,"line":384},[191,915,793],{"class":237},[191,917,918],{"class":200},"\"--send\"",[191,920,591],{"class":237},[191,922,895],{"class":305},[191,924,309],{"class":233},[191,926,900],{"class":200},[191,928,591],{"class":237},[191,930,809],{"class":305},[191,932,309],{"class":233},[191,934,935],{"class":200},"\"deliver the reports (off by default)\"",[191,937,315],{"class":237},[191,939,940,943,945],{"class":80,"line":389},[191,941,942],{"class":237},"    args ",[191,944,309],{"class":233},[191,946,947],{"class":237}," p.parse_args(argv)\n",[191,949,950,952,955,958,961,963,966,969,971,973],{"class":80,"line":400},[191,951,447],{"class":233},[191,953,954],{"class":237}," (args.start ",[191,956,957],{"class":233},"is",[191,959,960],{"class":284}," None",[191,962,413],{"class":237},[191,964,965],{"class":233},"!=",[191,967,968],{"class":237}," (args.end ",[191,970,957],{"class":233},[191,972,960],{"class":284},[191,974,768],{"class":237},[191,976,977,980,983],{"class":80,"line":444},[191,978,979],{"class":237},"        p.error(",[191,981,982],{"class":200},"\"--from and --to go together\"",[191,984,315],{"class":237},[191,986,987,989],{"class":80,"line":456},[191,988,339],{"class":233},[191,990,991],{"class":237}," args\n",[191,993,994],{"class":80,"line":496},[191,995,278],{"emptyLinePlaceholder":277},[191,997,998,1001,1003,1006,1008,1010,1013,1015,1017,1019,1022],{"class":80,"line":507},[191,999,1000],{"class":237},"args ",[191,1002,309],{"class":233},[191,1004,1005],{"class":237}," parse_args([",[191,1007,823],{"class":200},[191,1009,591],{"class":237},[191,1011,1012],{"class":200},"\"2026-09-14\"",[191,1014,591],{"class":237},[191,1016,857],{"class":200},[191,1018,591],{"class":237},[191,1020,1021],{"class":200},"\"2026-09-18\"",[191,1023,1024],{"class":237},"])\n",[191,1026,1027,1029],{"class":80,"line":542},[191,1028,676],{"class":284},[191,1030,1031],{"class":237},"(args.start, args.end, args.force, args.send)\n",[10,1033,1034,1035,1038],{},"Delivery is off unless ",[188,1036,1037],{},"--send"," is given. The scheduled job passes it explicitly; a backfill leaves it off, so regenerating a month of reports does not email thirty old ones to the finance team. The two ways of calling the script then look like this:",[181,1040,1042],{"className":183,"code":1041,"language":185,"meta":186,"style":186},"# the scheduled run: today's report, delivered\npython daily_sales.py --send\n# a backfill: September regenerated, nothing sent\npython daily_sales.py --from 2026-09-01 --to 2026-09-30\n",[188,1043,1044,1050,1060,1065],{"__ignoreMap":186},[191,1045,1046],{"class":80,"line":193},[191,1047,1049],{"class":1048},"s-wDw","# the scheduled run: today's report, delivered\n",[191,1051,1052,1054,1057],{"class":80,"line":247},[191,1053,226],{"class":196},[191,1055,1056],{"class":200}," daily_sales.py",[191,1058,1059],{"class":284}," --send\n",[191,1061,1062],{"class":80,"line":260},[191,1063,1064],{"class":1048},"# a backfill: September regenerated, nothing sent\n",[191,1066,1067,1069,1071,1074,1077,1080],{"class":80,"line":274},[191,1068,226],{"class":196},[191,1070,1056],{"class":200},[191,1072,1073],{"class":284}," --from",[191,1075,1076],{"class":200}," 2026-09-01",[191,1078,1079],{"class":284}," --to",[191,1081,1082],{"class":200}," 2026-09-30\n",[10,1084,1085],{},"Keeping one entry point matters: a separate backfill script drifts from the scheduled one, and the history it produces stops matching what the live job would have made.",[176,1087,1089],{"id":1088},"step-3-build-the-list-of-dates","Step 3 — Build the list of dates",[10,1091,1092],{},"A daily business report needs business days; a month-end report needs month ends. Let pandas build the list, so weekends — and, with a custom calendar, holidays — are skipped the same way the scheduler skips them:",[181,1094,1096],{"className":224,"code":1095,"language":226,"meta":186,"style":186},"def dates_to_run(args, today=date(2026, 10, 1)):\n    if args.start:\n        return [d.date() for d in pd.bdate_range(args.start, args.end)]\n    return [args.date or today]\n\nprint([f\"{d:%a %d}\" for d in dates_to_run(args)])\nprint([f\"{d:%Y-%m-%d}\" for d in pd.date_range(\"2026-01-01\", \"2026-12-31\", freq=\"BME\").date][:3], \"…  (month ends)\")\n",[188,1097,1098,1128,1135,1155,1168,1172,1211],{"__ignoreMap":186},[191,1099,1100,1102,1105,1108,1110,1113,1115,1117,1120,1122,1125],{"class":80,"line":193},[191,1101,326],{"class":233},[191,1103,1104],{"class":329}," dates_to_run",[191,1106,1107],{"class":237},"(args, today",[191,1109,309],{"class":233},[191,1111,1112],{"class":237},"date(",[191,1114,682],{"class":284},[191,1116,591],{"class":237},[191,1118,1119],{"class":284},"10",[191,1121,591],{"class":237},[191,1123,1124],{"class":284},"1",[191,1126,1127],{"class":237},")):\n",[191,1129,1130,1132],{"class":80,"line":247},[191,1131,447],{"class":233},[191,1133,1134],{"class":237}," args.start:\n",[191,1136,1137,1140,1143,1146,1149,1152],{"class":80,"line":260},[191,1138,1139],{"class":233},"        return",[191,1141,1142],{"class":237}," [d.date() ",[191,1144,1145],{"class":233},"for",[191,1147,1148],{"class":237}," d ",[191,1150,1151],{"class":233},"in",[191,1153,1154],{"class":237}," pd.bdate_range(args.start, args.end)]\n",[191,1156,1157,1159,1162,1165],{"class":80,"line":274},[191,1158,339],{"class":233},[191,1160,1161],{"class":237}," [args.date ",[191,1163,1164],{"class":233},"or",[191,1166,1167],{"class":237}," today]\n",[191,1169,1170],{"class":80,"line":281},[191,1171,278],{"emptyLinePlaceholder":277},[191,1173,1174,1176,1179,1181,1183,1185,1188,1190,1193,1195,1197,1199,1201,1204,1206,1208],{"class":80,"line":318},[191,1175,676],{"class":284},[191,1177,1178],{"class":237},"([",[191,1180,468],{"class":233},[191,1182,491],{"class":200},[191,1184,355],{"class":354},[191,1186,1187],{"class":237},"d:",[191,1189,361],{"class":233},[191,1191,1192],{"class":237},"a ",[191,1194,361],{"class":233},[191,1196,375],{"class":237},[191,1198,378],{"class":354},[191,1200,491],{"class":200},[191,1202,1203],{"class":233}," for",[191,1205,1148],{"class":237},[191,1207,1151],{"class":233},[191,1209,1210],{"class":237}," dates_to_run(args)])\n",[191,1212,1213,1215,1217,1219,1221,1223,1225,1227,1229,1231,1233,1235,1237,1239,1241,1243,1245,1247,1250,1253,1255,1258,1260,1263,1265,1268,1271,1273,1276,1279],{"class":80,"line":323},[191,1214,676],{"class":284},[191,1216,1178],{"class":237},[191,1218,468],{"class":233},[191,1220,491],{"class":200},[191,1222,355],{"class":354},[191,1224,1187],{"class":237},[191,1226,361],{"class":233},[191,1228,364],{"class":237},[191,1230,367],{"class":233},[191,1232,370],{"class":237},[191,1234,367],{"class":233},[191,1236,375],{"class":237},[191,1238,378],{"class":354},[191,1240,491],{"class":200},[191,1242,1203],{"class":233},[191,1244,1148],{"class":237},[191,1246,1151],{"class":233},[191,1248,1249],{"class":237}," pd.date_range(",[191,1251,1252],{"class":200},"\"2026-01-01\"",[191,1254,591],{"class":237},[191,1256,1257],{"class":200},"\"2026-12-31\"",[191,1259,591],{"class":237},[191,1261,1262],{"class":305},"freq",[191,1264,309],{"class":233},[191,1266,1267],{"class":200},"\"BME\"",[191,1269,1270],{"class":237},").date][:",[191,1272,692],{"class":284},[191,1274,1275],{"class":237},"], ",[191,1277,1278],{"class":200},"\"…  (month ends)\"",[191,1280,315],{"class":237},[10,1282,1283,1286,1287,1290,1291,1294,1295,1299],{},[188,1284,1285],{},"bdate_range"," gives Monday to Friday; ",[188,1288,1289],{},"freq=\"BME\""," gives the last weekday of each month. For holiday-aware business days, pass ",[188,1292,1293],{},"freq=pd.offsets.CustomBusinessDay(holidays=...)",", as in ",[14,1296,1298],{"href":1297},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-report-on-the-last-business-day-of-the-month\u002F","Run a Report on the Last Business Day of the Month",".",[176,1301,1303],{"id":1302},"step-4-run-the-range-skip-force-and-keep-going","Step 4 — Run the range: skip, force and keep going",[10,1305,1306,1307,1310],{},"Loop over the dates, skipping any whose report already exists unless ",[188,1308,1309],{},"--force"," was given, and catching failures so one bad day does not stop the rest. Collect the outcome of every date:",[181,1312,1314],{"className":224,"code":1313,"language":226,"meta":186,"style":186},"def backfill(argv):\n    args = parse_args(argv)\n    results = []\n    for d in dates_to_run(args):\n        if output_path(d).exists() and not args.force:\n            results.append((d, \"skipped\", \"already exists\"))\n            continue\n        try:\n            generate_report(d)\n            results.append((d, \"made\", \"\"))\n        except Exception as exc:\n            results.append((d, \"failed\", f\"{type(exc).__name__}: {exc}\"))\n    return pd.DataFrame(results, columns=[\"date\", \"status\", \"detail\"])\n\nsummary = backfill([\"--from\", \"2026-09-01\", \"--to\", \"2026-09-30\"])\nprint(summary[\"status\"].value_counts().to_dict())\nprint(summary[summary[\"status\"] != \"made\"].to_string(index=False))\n",[188,1315,1316,1326,1335,1345,1357,1373,1389,1394,1402,1407,1421,1435,1473,1503,1507,1535,1547],{"__ignoreMap":186},[191,1317,1318,1320,1323],{"class":80,"line":193},[191,1319,326],{"class":233},[191,1321,1322],{"class":329}," backfill",[191,1324,1325],{"class":237},"(argv):\n",[191,1327,1328,1330,1332],{"class":80,"line":247},[191,1329,942],{"class":237},[191,1331,309],{"class":233},[191,1333,1334],{"class":237}," parse_args(argv)\n",[191,1336,1337,1340,1342],{"class":80,"line":260},[191,1338,1339],{"class":237},"    results ",[191,1341,309],{"class":233},[191,1343,1344],{"class":237}," []\n",[191,1346,1347,1350,1352,1354],{"class":80,"line":274},[191,1348,1349],{"class":233},"    for",[191,1351,1148],{"class":237},[191,1353,1151],{"class":233},[191,1355,1356],{"class":237}," dates_to_run(args):\n",[191,1358,1359,1362,1365,1368,1370],{"class":80,"line":281},[191,1360,1361],{"class":233},"        if",[191,1363,1364],{"class":237}," output_path(d).exists() ",[191,1366,1367],{"class":233},"and",[191,1369,450],{"class":233},[191,1371,1372],{"class":237}," args.force:\n",[191,1374,1375,1378,1381,1383,1386],{"class":80,"line":318},[191,1376,1377],{"class":237},"            results.append((d, ",[191,1379,1380],{"class":200},"\"skipped\"",[191,1382,591],{"class":237},[191,1384,1385],{"class":200},"\"already exists\"",[191,1387,1388],{"class":237},"))\n",[191,1390,1391],{"class":80,"line":323},[191,1392,1393],{"class":233},"            continue\n",[191,1395,1396,1399],{"class":80,"line":336},[191,1397,1398],{"class":233},"        try",[191,1400,1401],{"class":237},":\n",[191,1403,1404],{"class":80,"line":384},[191,1405,1406],{"class":237},"            generate_report(d)\n",[191,1408,1409,1411,1414,1416,1419],{"class":80,"line":389},[191,1410,1377],{"class":237},[191,1412,1413],{"class":200},"\"made\"",[191,1415,591],{"class":237},[191,1417,1418],{"class":200},"\"\"",[191,1420,1388],{"class":237},[191,1422,1423,1426,1429,1432],{"class":80,"line":400},[191,1424,1425],{"class":233},"        except",[191,1427,1428],{"class":284}," Exception",[191,1430,1431],{"class":233}," as",[191,1433,1434],{"class":237}," exc:\n",[191,1436,1437,1439,1442,1444,1446,1448,1450,1452,1455,1458,1460,1462,1464,1467,1469,1471],{"class":80,"line":444},[191,1438,1377],{"class":237},[191,1440,1441],{"class":200},"\"failed\"",[191,1443,591],{"class":237},[191,1445,468],{"class":233},[191,1447,491],{"class":200},[191,1449,355],{"class":354},[191,1451,801],{"class":284},[191,1453,1454],{"class":237},"(exc).",[191,1456,1457],{"class":284},"__name__",[191,1459,378],{"class":354},[191,1461,481],{"class":200},[191,1463,355],{"class":354},[191,1465,1466],{"class":237},"exc",[191,1468,378],{"class":354},[191,1470,491],{"class":200},[191,1472,1388],{"class":237},[191,1474,1475,1477,1480,1483,1485,1488,1491,1493,1496,1498,1501],{"class":80,"line":456},[191,1476,339],{"class":233},[191,1478,1479],{"class":237}," pd.DataFrame(results, ",[191,1481,1482],{"class":305},"columns",[191,1484,309],{"class":233},[191,1486,1487],{"class":237},"[",[191,1489,1490],{"class":200},"\"date\"",[191,1492,591],{"class":237},[191,1494,1495],{"class":200},"\"status\"",[191,1497,591],{"class":237},[191,1499,1500],{"class":200},"\"detail\"",[191,1502,1024],{"class":237},[191,1504,1505],{"class":80,"line":496},[191,1506,278],{"emptyLinePlaceholder":277},[191,1508,1509,1512,1514,1517,1519,1521,1524,1526,1528,1530,1533],{"class":80,"line":507},[191,1510,1511],{"class":237},"summary ",[191,1513,309],{"class":233},[191,1515,1516],{"class":237}," backfill([",[191,1518,823],{"class":200},[191,1520,591],{"class":237},[191,1522,1523],{"class":200},"\"2026-09-01\"",[191,1525,591],{"class":237},[191,1527,857],{"class":200},[191,1529,591],{"class":237},[191,1531,1532],{"class":200},"\"2026-09-30\"",[191,1534,1024],{"class":237},[191,1536,1537,1539,1542,1544],{"class":80,"line":542},[191,1538,676],{"class":284},[191,1540,1541],{"class":237},"(summary[",[191,1543,1495],{"class":200},[191,1545,1546],{"class":237},"].value_counts().to_dict())\n",[191,1548,1549,1551,1554,1556,1558,1560,1563,1566,1568,1570,1572],{"class":80,"line":553},[191,1550,676],{"class":284},[191,1552,1553],{"class":237},"(summary[summary[",[191,1555,1495],{"class":200},[191,1557,529],{"class":237},[191,1559,965],{"class":233},[191,1561,1562],{"class":200}," \"made\"",[191,1564,1565],{"class":237},"].to_string(",[191,1567,594],{"class":305},[191,1569,309],{"class":233},[191,1571,599],{"class":284},[191,1573,1388],{"class":237},[10,1575,1576,1577,1580],{},"Twenty-two business days: twenty made, one skipped because 3 September was produced in Step 1, and one failure — the 17th has no input. The failure is recorded with its reason and the backfill carries on. Skipping existing reports makes the backfill ",[700,1578,1579],{},"restartable",": if it is interrupted after ten days, running the same command again picks up where it stopped.",[20,1582,29,1588,29,1591,29,1594,29,1598,29,1603,29,1612,29,1618,29,1625,29,1630,29,1633,29,1637,29,1640,29,1644,29,1647,29,1651,29,1655],{"viewBox":1583,"role":23,"ariaLabelledBy":1584,"xmlns":27,"style":1587},"4 0 744 172",[1585,1586],"bf-outcome-t","bf-outcome-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1589,1590],{"id":1585},"Outcome of backfilling September 2026",[35,1592,1593],{"id":1586},"Bar chart of the backfill of 22 business days in September 2026: 20 reports made, 1 skipped because it already existed and 1 failed because its input file was missing.",[39,1595],{"x":41,"y":42,"width":1596,"height":1597,"fill":45},"744","172",[68,1599,1602],{"x":124,"y":1600,"style":1601},"31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","22 business days",[39,1604],{"x":48,"y":1605,"width":1606,"height":1607,"rx":1608,"fill":1609,"stroke":1610,"style":1611},"48.0","79.1","28.0","7","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[68,1613,1617],{"x":1614,"y":1615,"style":1616},"59.6","66.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","made",[39,1619],{"x":1620,"y":1605,"width":1621,"height":1607,"rx":1608,"fill":1622,"stroke":1623,"style":1624},"109.1","598.4","#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[68,1626,1629],{"x":1627,"y":1615,"style":1628},"715.6","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","20",[39,1631],{"x":48,"y":1632,"width":1606,"height":1607,"rx":1608,"fill":1609,"stroke":1610,"style":1611},"88.0",[68,1634,1636],{"x":1614,"y":1635,"style":1616},"106.5","skipped",[39,1638],{"x":1620,"y":1632,"width":1639,"height":1607,"rx":1608,"fill":1609,"stroke":1610,"style":1624},"29.9",[68,1641,1124],{"x":1642,"y":1635,"style":1643},"147.1","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[39,1645],{"x":48,"y":1646,"width":1606,"height":1607,"rx":1608,"fill":1609,"stroke":1610,"style":1611},"128.0",[68,1648,1650],{"x":1614,"y":1649,"style":1616},"146.5","failed",[39,1652],{"x":1620,"y":1646,"width":1639,"height":1607,"rx":1608,"fill":1653,"stroke":1654,"style":1624},"#fee8f2","var(--accent,#f43f8f)",[68,1656,1124],{"x":1642,"y":1649,"style":1657},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start",[176,1659,1661],{"id":1660},"step-5-rerun-only-what-failed","Step 5 — Rerun only what failed",[10,1663,1664,1665,734],{},"Once the missing input for the 17th has been restored, rerun just the failed dates. The summary already lists them; pass each one as ",[188,1666,718],{},[181,1668,1670],{"className":224,"code":1669,"language":226,"meta":186,"style":186},"failed = summary.loc[summary[\"status\"] == \"failed\", \"date\"].tolist()\npd.DataFrame({\"Region\": [\"North\", \"South\", \"East\", \"West\"], \"Revenue\": [15100, 9800, 17250, 11900]}).to_excel(\n    \"inputs\u002Fsales_2026-09-17.xlsx\", index=False)\n\nretry = pd.concat([backfill([\"--date\", str(d)]) for d in failed], ignore_index=True)\nprint(retry.to_string(index=False))\nprint(len(list(OUT.glob(\"daily_sales_2026-09-*.xlsx\"))), \"September reports on disk\")\n",[188,1671,1672,1699,1755,1770,1774,1812,1827],{"__ignoreMap":186},[191,1673,1674,1677,1679,1682,1684,1686,1689,1692,1694,1696],{"class":80,"line":193},[191,1675,1676],{"class":237},"failed ",[191,1678,309],{"class":233},[191,1680,1681],{"class":237}," summary.loc[summary[",[191,1683,1495],{"class":200},[191,1685,529],{"class":237},[191,1687,1688],{"class":233},"==",[191,1690,1691],{"class":200}," \"failed\"",[191,1693,591],{"class":237},[191,1695,1490],{"class":200},[191,1697,1698],{"class":237},"].tolist()\n",[191,1700,1701,1704,1707,1710,1713,1715,1718,1720,1723,1725,1728,1730,1732,1734,1737,1739,1742,1744,1747,1749,1752],{"class":80,"line":247},[191,1702,1703],{"class":237},"pd.DataFrame({",[191,1705,1706],{"class":200},"\"Region\"",[191,1708,1709],{"class":237},": [",[191,1711,1712],{"class":200},"\"North\"",[191,1714,591],{"class":237},[191,1716,1717],{"class":200},"\"South\"",[191,1719,591],{"class":237},[191,1721,1722],{"class":200},"\"East\"",[191,1724,591],{"class":237},[191,1726,1727],{"class":200},"\"West\"",[191,1729,1275],{"class":237},[191,1731,526],{"class":200},[191,1733,1709],{"class":237},[191,1735,1736],{"class":284},"15100",[191,1738,591],{"class":237},[191,1740,1741],{"class":284},"9800",[191,1743,591],{"class":237},[191,1745,1746],{"class":284},"17250",[191,1748,591],{"class":237},[191,1750,1751],{"class":284},"11900",[191,1753,1754],{"class":237},"]}).to_excel(\n",[191,1756,1757,1760,1762,1764,1766,1768],{"class":80,"line":260},[191,1758,1759],{"class":200},"    \"inputs\u002Fsales_2026-09-17.xlsx\"",[191,1761,591],{"class":237},[191,1763,594],{"class":305},[191,1765,309],{"class":233},[191,1767,599],{"class":284},[191,1769,315],{"class":237},[191,1771,1772],{"class":80,"line":274},[191,1773,278],{"emptyLinePlaceholder":277},[191,1775,1776,1779,1781,1784,1786,1788,1791,1794,1796,1798,1800,1803,1806,1808,1810],{"class":80,"line":281},[191,1777,1778],{"class":237},"retry ",[191,1780,309],{"class":233},[191,1782,1783],{"class":237}," pd.concat([backfill([",[191,1785,796],{"class":200},[191,1787,591],{"class":237},[191,1789,1790],{"class":284},"str",[191,1792,1793],{"class":237},"(d)]) ",[191,1795,1145],{"class":233},[191,1797,1148],{"class":237},[191,1799,1151],{"class":233},[191,1801,1802],{"class":237}," failed], ",[191,1804,1805],{"class":305},"ignore_index",[191,1807,309],{"class":233},[191,1809,312],{"class":284},[191,1811,315],{"class":237},[191,1813,1814,1816,1819,1821,1823,1825],{"class":80,"line":318},[191,1815,676],{"class":284},[191,1817,1818],{"class":237},"(retry.to_string(",[191,1820,594],{"class":305},[191,1822,309],{"class":233},[191,1824,599],{"class":284},[191,1826,1388],{"class":237},[191,1828,1829,1831,1833,1836,1838,1841,1843,1845,1848,1851,1854,1857],{"class":80,"line":323},[191,1830,676],{"class":284},[191,1832,465],{"class":237},[191,1834,1835],{"class":284},"len",[191,1837,465],{"class":237},[191,1839,1840],{"class":284},"list",[191,1842,465],{"class":237},[191,1844,285],{"class":284},[191,1846,1847],{"class":237},".glob(",[191,1849,1850],{"class":200},"\"daily_sales_2026-09-*.xlsx\"",[191,1852,1853],{"class":237},"))), ",[191,1855,1856],{"class":200},"\"September reports on disk\"",[191,1858,315],{"class":237},[10,1860,1861,1862,1865],{},"All 22 September reports now exist. Save each backfill's summary to a file — ",[188,1863,1864],{},"summary.to_csv(f\"backfill_{start}_{end}.csv\")"," — so there is a record of what was regenerated, when, and what failed.",[176,1867,1869],{"id":1868},"point-in-time-inputs","Point-in-time inputs",[10,1871,1872,1873,1877],{},"A backfill is only as correct as its inputs. Reports that read one file per day, as here, are easy: the input for 3 September is the file for 3 September. Reports that read a ",[1874,1875,1876],"em",{},"current"," table — today's customer list, today's price list, a database table updated in place — will silently use today's reference data for every past date, and produce history that never existed. Before backfilling such a report, decide what \"as of 3 September\" means for each input: filter a history table by its effective dates, read an archived snapshot, or accept and document that reference data is current. Keeping a dated copy of every input on every scheduled run is the cheapest way to make future backfills exact.",[20,1879,29,1884,29,1887,29,1890,29,1893,29,1898,29,1905,29,1911,29,1916,29,1922,29,1928,29,1932,29,1937,29,1941,29,1946,29,1951,29,1958,29,1963,29,1967,29,1971,29,1977,29,1980,29,1985,29,1988],{"viewBox":1880,"role":23,"ariaLabelledBy":1881,"xmlns":27,"style":28},"4 0 752 207",[1882,1883],"bf-inputs-t","bf-inputs-d",[31,1885,1886],{"id":1882},"Daily inputs versus current tables in a backfill",[35,1888,1889],{"id":1883},"Two panels. With one input file per day, the backfill for 3 September reads the file for 3 September and reproduces the report exactly. With a current table updated in place, the backfill for 3 September reads today's data, so customers, prices or mappings that changed since then are wrong in every past report.",[39,1891],{"x":41,"y":42,"width":43,"height":1892,"fill":45},"207",[39,1894],{"x":48,"y":49,"width":1895,"height":1896,"rx":1897,"fill":1622,"stroke":1623,"style":55},"351.0","175.0","14",[57,1899,1901],{"transform":1900},"translate(182.5,29.0) scale(1.083)",[61,1902],{"d":167,"fill":64,"stroke":1903,"style":1904},"var(--teal-ink,#0b6157)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[68,1906,1910],{"x":1907,"y":1908,"style":1909},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","One input per day",[80,1912],{"x1":1913,"y1":1914,"x2":1915,"y2":1914,"stroke":1623,"style":1611},"36.0","91.0","355.0",[68,1917,1921],{"x":1918,"y":1919,"style":1920},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","reads sales_2026-09-03.xlsx",[57,1923,1925],{"transform":1924},"translate(38.5,99.5) scale(0.625)",[61,1926],{"d":167,"fill":64,"stroke":1623,"style":1927},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[68,1929,1931],{"x":1918,"y":1930,"style":1920},"137.0","reproduces the original",[57,1933,1935],{"transform":1934},"translate(38.5,125.5) scale(0.625)",[61,1936],{"d":167,"fill":64,"stroke":1623,"style":1927},[68,1938,1940],{"x":1918,"y":1939,"style":1920},"163.0","safe to rerun any time",[57,1942,1944],{"transform":1943},"translate(38.5,151.5) scale(0.625)",[61,1945],{"d":167,"fill":64,"stroke":1623,"style":1927},[39,1947],{"x":1948,"y":49,"width":1895,"height":1896,"rx":1897,"fill":1949,"stroke":1950,"style":55},"389.0","#fdefd8","var(--gold,#b4740a)",[57,1952,1954],{"transform":1953},"translate(551.5,29.0) scale(1.083)",[61,1955],{"d":1956,"fill":64,"stroke":1957,"style":1904},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[68,1959,1962],{"x":1960,"y":1908,"style":1961},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","A current table",[80,1964],{"x1":1965,"y1":1914,"x2":1966,"y2":1914,"stroke":1950,"style":1611},"405.0","724.0",[68,1968,1970],{"x":1969,"y":1919,"style":1920},"429.0","reads today's customers and prices",[57,1972,1974],{"transform":1973},"translate(407.5,99.5) scale(0.625)",[61,1975],{"d":1976,"fill":64,"stroke":1654,"style":1927},"M6 6l12 12 M18 6L6 18",[68,1978,1979],{"x":1969,"y":1930,"style":1920},"history that never existed",[57,1981,1983],{"transform":1982},"translate(407.5,125.5) scale(0.625)",[61,1984],{"d":1976,"fill":64,"stroke":1654,"style":1927},[68,1986,1987],{"x":1969,"y":1939,"style":1920},"needs snapshots or effective dates",[57,1989,1991],{"transform":1990},"translate(407.5,151.5) scale(0.625)",[61,1992],{"d":1956,"fill":64,"stroke":1950,"style":1927},[176,1994,1996],{"id":1995},"common-pitfalls","Common pitfalls",[1998,1999,2000,2011,2017,2023,2032,2038],"ul",{},[2001,2002,2003,2010],"li",{},[700,2004,2005,2006,2009],{},"Calling ",[188,2007,2008],{},"date.today()"," inside the logic."," Pass the as-of date in; only the entry point reads the clock.",[2001,2012,2013,2016],{},[700,2014,2015],{},"Naming output by run date."," Name it by the as-of date so reruns replace, not duplicate.",[2001,2018,2019,2022],{},[700,2020,2021],{},"Stopping at the first failure."," Catch per date and summarise at the end.",[2001,2024,2025,2028,2029,2031],{},[700,2026,2027],{},"Overwriting by default."," Skip existing reports unless ",[188,2030,1309],{},", so restarts are safe.",[2001,2033,2034,2037],{},[700,2035,2036],{},"Emailing during a backfill."," Keep delivery off unless explicitly requested.",[2001,2039,2040,2043],{},[700,2041,2042],{},"Current reference data for past dates."," Use snapshots or effective-dated history.",[176,2045,2047],{"id":2046},"performance-notes","Performance notes",[10,2049,2050,2051,1294,2054,2058],{},"Dates are independent, so a long backfill can run several at once with ",[188,2052,2053],{},"concurrent.futures.ProcessPoolExecutor",[14,2055,2057],{"href":2056},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fprocess-multiple-excel-files-in-parallel-with-python\u002F","Process Multiple Excel Files in Parallel with Python"," — but watch the load on shared databases, and take a per-date lock if a scheduled run could overlap.",[176,2060,2062],{"id":2061},"conclusion","Conclusion",[10,2064,2065,2066,591,2068,416,2070,591,2072,2074,2075,2077,2078,2080],{},"Make the report a function of an as-of date and derive every date, filter and file name from it. Give the script ",[188,2067,718],{},[188,2069,722],{},[188,2071,725],{},[188,2073,1309],{}," and an explicit ",[188,2076,1037],{},", build the date list with ",[188,2079,1285],{}," or a month-end frequency, and loop over it skipping existing outputs and catching failures per date. Rerun only the failures once fixed, keep a summary of every backfill, and make sure past dates read past inputs.",[176,2082,2084],{"id":2083},"frequently-asked-questions","Frequently asked questions",[10,2086,2087,2090],{},[700,2088,2089],{},"How do I rerun a Python report for a past date?","\nGive the report an as-of date parameter and derive every date, file name and filter from it, instead of calling datetime.now() or date.today() inside the logic. Then running it for 3 September is just running it with --date 2026-09-03.",[10,2092,2093,2096],{},[700,2094,2095],{},"How do I backfill a report for every business day in a range?","\nBuild the list of dates with pandas.bdate_range(start, end) — or a holiday-aware calendar — and call the report function for each date, collecting successes and failures, rather than looping in a shell script.",[10,2098,2099,2102],{},[700,2100,2101],{},"Should a backfill overwrite reports that already exist?","\nNot by default. Skip dates whose output already exists, so an interrupted backfill can simply be restarted, and provide a --force option for when the point of the backfill is to replace reports produced by buggy code.",[10,2104,2105,2108],{},[700,2106,2107],{},"How do I stop a backfill emailing thirty old reports?","\nSeparate generating a report from delivering it, and make delivery a switch. A backfill runs with delivery off, or delivery to a test address, and a person decides which regenerated reports to send.",[176,2110,2112],{"id":2111},"related","Related",[1998,2114,2115,2121,2128,2135],{},[2001,2116,2117,2118,2120],{},"Up one level: ",[14,2119,17],{"href":16}," — running reports on a schedule.",[2001,2122,2123,2127],{},[14,2124,2126],{"href":2125},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002F","Prevent Overlapping Runs of an Excel Report Job"," — per-date locks for backfills.",[2001,2129,2130,2134],{},[14,2131,2133],{"href":2132},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fmake-excel-output-reproducible\u002F","Make Excel Output Reproducible"," — rerun, get the same file.",[2001,2136,2137,2141],{},[14,2138,2140],{"href":2139},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002F","Build a Command-Line Tool for Excel Reports with argparse"," — argparse in depth.",[2143,2144,2145],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":186,"searchDepth":247,"depth":247,"links":2147},[2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159],{"id":178,"depth":247,"text":179},{"id":217,"depth":247,"text":218},{"id":711,"depth":247,"text":712},{"id":1088,"depth":247,"text":1089},{"id":1302,"depth":247,"text":1303},{"id":1660,"depth":247,"text":1661},{"id":1868,"depth":247,"text":1869},{"id":1995,"depth":247,"text":1996},{"id":2046,"depth":247,"text":2047},{"id":2061,"depth":247,"text":2062},{"id":2083,"depth":247,"text":2084},{"id":2111,"depth":247,"text":2112},"2026-09-25","Regenerate a daily or monthly report for a range of past dates — after a bug fix, a missed week or a new report that needs history. Make the report take an as-of date instead of reading the clock, give it a date-range command line, skip what already exists, keep going past failures, rerun only what failed, and never email anyone during a backfill.","md",[2164,2166,2168,2170],{"q":2089,"a":2165},"Give the report an as-of date parameter and derive every date, file name and filter from it, instead of calling datetime.now() or date.today() inside the logic. Then running it for 3 September is just running it with --date 2026-09-03.",{"q":2095,"a":2167},"Build the list of dates with pandas.bdate_range(start, end) — or a holiday-aware calendar — and call the report function for each date, collecting successes and failures, rather than looping in a shell script.",{"q":2101,"a":2169},"Not by default. Skip dates whose output already exists, so an interrupted backfill can simply be restarted, and provide a --force option for when the point of the backfill is to replace reports produced by buggy code.",{"q":2107,"a":2171},"Separate generating a report from delivering it, and make delivery a switch. A backfill runs with delivery off, or delivery to a test address, and a person decides which regenerated reports to send.","backfill excel reports for past dates",null,{"breadcrumb":2175},[2176,2178,2181],{"name":2177,"item":416},"Home",{"name":2179,"item":2180},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates",{"title":2184,"description":2185},"Backfill Python Excel Reports for a Range of Past Dates","Backfill Excel reports safely: an as-of date parameter instead of datetime.now, argparse date ranges, business-day and month-end date lists, idempotent output names, skip or force, point-in-time inputs, failure summaries, retrying failures and a no-send switch.","backfill-excel-reports-for-past-dates","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates\u002Findex",[2189,2190],"scheduling","reliable-jobs","how-to","ton1p41ms6obKCrMzxDVxuulOzZ8XBp6bmG4eNGGWFk",[2194,2197],{"title":17,"path":2195,"stem":2196,"children":-1},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Findex",{"title":2198,"path":2199,"stem":2200,"children":-1},"Choose a Scheduler for Python Excel Reports","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports\u002Findex",[2202,2204],{"path":2203,"title":2179},"\u002Fautomating-reporting-workflows",{"path":2195,"title":17},[],[2207,2210,2213,2216,2219,2222],{"path":2208,"title":2209,"kind":2191},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":2211,"title":2212,"kind":2191},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly","Read Excel Attachments into a Database Nightly",{"path":2214,"title":2215,"kind":2191},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule","Refresh an Excel Report from a Database on a Schedule",{"path":2217,"title":2218,"kind":2191},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive","Run a Report When an Excel File Changes in OneDrive",{"path":2220,"title":2221,"kind":2191},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-report-emails-at-each-recipients-local-time","Send Report Emails at Each Recipient's Local Time",{"path":2223,"title":2224,"kind":2191},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",1790972138601]