[{"data":1,"prerenderedAt":2740},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report":2707,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report":2716,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report":2721,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report":2722},{"id":4,"title":5,"body":6,"dateModified":2674,"datePublished":2674,"description":2675,"extension":2676,"faq":2677,"keyword":2686,"kind":2687,"meta":2688,"navigation":122,"path":2696,"seo":2697,"slug":2700,"stem":2701,"topics":2702,"type":2705,"__hash__":2706},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report\u002Findex.md","Wait for Upstream Data Before Running a Report",{"type":7,"value":8,"toc":2662},"minimark",[9,19,24,53,56,60,689,697,704,862,866,1150,1153,1157,1456,1463,1596,1600,2127,2138,2315,2318,2544,2548,2551,2555,2590,2594,2597,2601,2607,2613,2619,2625,2629,2658],[10,11,12,13,18],"p",{},"A report scheduled for 07:00 assumes that the data it needs is there by 07:00. Most days it is. On the day the ERP export runs late, or the overnight database load fails halfway, the report still runs on time — and quietly produces figures from yesterday's file or from a half-loaded table. Nothing errors, the email goes out, and the wrong numbers are discussed in the morning meeting. The cure is to make the report check that its inputs are ready before it builds anything, wait a while if they are not, and give up at a deadline with a clear message instead of a wrong report. This page builds those readiness checks for the three common kinds of input — a file, a marker, a database table — a polling loop with a deadline, and a test that runs the whole thing against a simulated clock. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55],{},"The examples use a local folder for the export file and a SQLite database with a load-status table, both created on the fly. The same checks work against a network share, an SFTP folder or a production database with the connection changed.",[20,57,59],{"id":58},"step-1-write-each-readiness-check-as-a-small-function","Step 1 — Write each readiness check as a small function",[25,61,65],{"className":62,"code":63,"language":64,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import sqlite3\nfrom contextlib import closing\nfrom datetime import date, datetime, timedelta\nfrom pathlib import Path\n\ndef export_file_ready(folder, business_date, min_age_s=120, now=None):\n    \"\"\"The export for the business date exists, is non-empty and has not changed for min_age_s.\"\"\"\n    now = now or datetime.now()\n    path = Path(folder) \u002F f\"sales_{business_date:%Y%m%d}.xlsx\"\n    if not path.exists():\n        return False, f\"{path.name} not there yet\"\n    st = path.stat()\n    if st.st_size == 0:\n        return False, f\"{path.name} is empty\"\n    age = now.timestamp() - st.st_mtime\n    if age \u003C min_age_s:\n        return False, f\"{path.name} changed {age:.0f}s ago, may still be copying\"\n    return True, f\"{path.name} ready ({st.st_size:,} bytes)\"\n\ndef marker_ready(folder, business_date):\n    marker = Path(folder) \u002F f\"sales_{business_date:%Y%m%d}.done\"\n    return marker.exists(), f\"{marker.name} {'found' if marker.exists() else 'missing'}\"\n\ndef table_loaded(db, table, business_date):\n    with closing(sqlite3.connect(db)) as con:\n        row = con.execute(\"SELECT status, rows FROM load_status WHERE table_name = ? AND business_date = ?\",\n                          (table, business_date.isoformat())).fetchone()\n    if row is None:\n        return False, f\"{table}: no load recorded for {business_date}\"\n    status, rows = row\n    return (status == \"complete\" and rows > 0), f\"{table}: {status}, {rows:,} rows\"\n","python",[32,66,67,77,91,104,117,124,155,161,178,227,239,267,278,295,317,334,348,383,420,425,436,473,515,520,531,546,563,569,585,617,628],{"__ignoreMap":30},[35,68,69,73],{"class":37,"line":38},[35,70,72],{"class":71},"s-kum","import",[35,74,76],{"class":75},"skGVy"," sqlite3\n",[35,78,80,83,86,88],{"class":37,"line":79},2,[35,81,82],{"class":71},"from",[35,84,85],{"class":75}," contextlib ",[35,87,72],{"class":71},[35,89,90],{"class":75}," closing\n",[35,92,94,96,99,101],{"class":37,"line":93},3,[35,95,82],{"class":71},[35,97,98],{"class":75}," datetime ",[35,100,72],{"class":71},[35,102,103],{"class":75}," date, datetime, timedelta\n",[35,105,107,109,112,114],{"class":37,"line":106},4,[35,108,82],{"class":71},[35,110,111],{"class":75}," pathlib ",[35,113,72],{"class":71},[35,115,116],{"class":75}," Path\n",[35,118,120],{"class":37,"line":119},5,[35,121,123],{"emptyLinePlaceholder":122},true,"\n",[35,125,127,130,134,137,140,144,147,149,152],{"class":37,"line":126},6,[35,128,129],{"class":71},"def",[35,131,133],{"class":132},"s_Opv"," export_file_ready",[35,135,136],{"class":75},"(folder, business_date, min_age_s",[35,138,139],{"class":71},"=",[35,141,143],{"class":142},"sP0c6","120",[35,145,146],{"class":75},", now",[35,148,139],{"class":71},[35,150,151],{"class":142},"None",[35,153,154],{"class":75},"):\n",[35,156,158],{"class":37,"line":157},7,[35,159,160],{"class":45},"    \"\"\"The export for the business date exists, is non-empty and has not changed for min_age_s.\"\"\"\n",[35,162,164,167,169,172,175],{"class":37,"line":163},8,[35,165,166],{"class":75},"    now ",[35,168,139],{"class":71},[35,170,171],{"class":75}," now ",[35,173,174],{"class":71},"or",[35,176,177],{"class":75}," datetime.now()\n",[35,179,181,184,186,189,192,195,198,202,205,208,211,213,216,218,221,224],{"class":37,"line":180},9,[35,182,183],{"class":75},"    path ",[35,185,139],{"class":71},[35,187,188],{"class":75}," Path(folder) ",[35,190,191],{"class":71},"\u002F",[35,193,194],{"class":71}," f",[35,196,197],{"class":45},"\"sales_",[35,199,201],{"class":200},"sSjpA","{",[35,203,204],{"class":75},"business_date:",[35,206,207],{"class":71},"%",[35,209,210],{"class":75},"Y",[35,212,207],{"class":71},[35,214,215],{"class":75},"m",[35,217,207],{"class":71},[35,219,220],{"class":75},"d",[35,222,223],{"class":200},"}",[35,225,226],{"class":45},".xlsx\"\n",[35,228,230,233,236],{"class":37,"line":229},10,[35,231,232],{"class":71},"    if",[35,234,235],{"class":71}," not",[35,237,238],{"class":75}," path.exists():\n",[35,240,242,245,248,251,254,257,259,262,264],{"class":37,"line":241},11,[35,243,244],{"class":71},"        return",[35,246,247],{"class":142}," False",[35,249,250],{"class":75},", ",[35,252,253],{"class":71},"f",[35,255,256],{"class":45},"\"",[35,258,201],{"class":200},[35,260,261],{"class":75},"path.name",[35,263,223],{"class":200},[35,265,266],{"class":45}," not there yet\"\n",[35,268,270,273,275],{"class":37,"line":269},12,[35,271,272],{"class":75},"    st ",[35,274,139],{"class":71},[35,276,277],{"class":75}," path.stat()\n",[35,279,281,283,286,289,292],{"class":37,"line":280},13,[35,282,232],{"class":71},[35,284,285],{"class":75}," st.st_size ",[35,287,288],{"class":71},"==",[35,290,291],{"class":142}," 0",[35,293,294],{"class":75},":\n",[35,296,298,300,302,304,306,308,310,312,314],{"class":37,"line":297},14,[35,299,244],{"class":71},[35,301,247],{"class":142},[35,303,250],{"class":75},[35,305,253],{"class":71},[35,307,256],{"class":45},[35,309,201],{"class":200},[35,311,261],{"class":75},[35,313,223],{"class":200},[35,315,316],{"class":45}," is empty\"\n",[35,318,320,323,325,328,331],{"class":37,"line":319},15,[35,321,322],{"class":75},"    age ",[35,324,139],{"class":71},[35,326,327],{"class":75}," now.timestamp() ",[35,329,330],{"class":71},"-",[35,332,333],{"class":75}," st.st_mtime\n",[35,335,337,339,342,345],{"class":37,"line":336},16,[35,338,232],{"class":71},[35,340,341],{"class":75}," age ",[35,343,344],{"class":71},"\u003C",[35,346,347],{"class":75}," min_age_s:\n",[35,349,351,353,355,357,359,361,363,365,367,370,372,375,378,380],{"class":37,"line":350},17,[35,352,244],{"class":71},[35,354,247],{"class":142},[35,356,250],{"class":75},[35,358,253],{"class":71},[35,360,256],{"class":45},[35,362,201],{"class":200},[35,364,261],{"class":75},[35,366,223],{"class":200},[35,368,369],{"class":45}," changed ",[35,371,201],{"class":200},[35,373,374],{"class":75},"age",[35,376,377],{"class":71},":.0f",[35,379,223],{"class":200},[35,381,382],{"class":45},"s ago, may still be copying\"\n",[35,384,386,389,392,394,396,398,400,402,404,407,409,412,415,417],{"class":37,"line":385},18,[35,387,388],{"class":71},"    return",[35,390,391],{"class":142}," True",[35,393,250],{"class":75},[35,395,253],{"class":71},[35,397,256],{"class":45},[35,399,201],{"class":200},[35,401,261],{"class":75},[35,403,223],{"class":200},[35,405,406],{"class":45}," ready (",[35,408,201],{"class":200},[35,410,411],{"class":75},"st.st_size",[35,413,414],{"class":71},":,",[35,416,223],{"class":200},[35,418,419],{"class":45}," bytes)\"\n",[35,421,423],{"class":37,"line":422},19,[35,424,123],{"emptyLinePlaceholder":122},[35,426,428,430,433],{"class":37,"line":427},20,[35,429,129],{"class":71},[35,431,432],{"class":132}," marker_ready",[35,434,435],{"class":75},"(folder, business_date):\n",[35,437,439,442,444,446,448,450,452,454,456,458,460,462,464,466,468,470],{"class":37,"line":438},21,[35,440,441],{"class":75},"    marker ",[35,443,139],{"class":71},[35,445,188],{"class":75},[35,447,191],{"class":71},[35,449,194],{"class":71},[35,451,197],{"class":45},[35,453,201],{"class":200},[35,455,204],{"class":75},[35,457,207],{"class":71},[35,459,210],{"class":75},[35,461,207],{"class":71},[35,463,215],{"class":75},[35,465,207],{"class":71},[35,467,220],{"class":75},[35,469,223],{"class":200},[35,471,472],{"class":45},".done\"\n",[35,474,476,478,481,483,485,487,490,492,495,498,501,504,507,510,512],{"class":37,"line":475},22,[35,477,388],{"class":71},[35,479,480],{"class":75}," marker.exists(), ",[35,482,253],{"class":71},[35,484,256],{"class":45},[35,486,201],{"class":200},[35,488,489],{"class":75},"marker.name",[35,491,223],{"class":200},[35,493,494],{"class":200}," {",[35,496,497],{"class":45},"'found'",[35,499,500],{"class":71}," if",[35,502,503],{"class":75}," marker.exists() ",[35,505,506],{"class":71},"else",[35,508,509],{"class":45}," 'missing'",[35,511,223],{"class":200},[35,513,514],{"class":45},"\"\n",[35,516,518],{"class":37,"line":517},23,[35,519,123],{"emptyLinePlaceholder":122},[35,521,523,525,528],{"class":37,"line":522},24,[35,524,129],{"class":71},[35,526,527],{"class":132}," table_loaded",[35,529,530],{"class":75},"(db, table, business_date):\n",[35,532,534,537,540,543],{"class":37,"line":533},25,[35,535,536],{"class":71},"    with",[35,538,539],{"class":75}," closing(sqlite3.connect(db)) ",[35,541,542],{"class":71},"as",[35,544,545],{"class":75}," con:\n",[35,547,549,552,554,557,560],{"class":37,"line":548},26,[35,550,551],{"class":75},"        row ",[35,553,139],{"class":71},[35,555,556],{"class":75}," con.execute(",[35,558,559],{"class":45},"\"SELECT status, rows FROM load_status WHERE table_name = ? AND business_date = ?\"",[35,561,562],{"class":75},",\n",[35,564,566],{"class":37,"line":565},27,[35,567,568],{"class":75},"                          (table, business_date.isoformat())).fetchone()\n",[35,570,572,574,577,580,583],{"class":37,"line":571},28,[35,573,232],{"class":71},[35,575,576],{"class":75}," row ",[35,578,579],{"class":71},"is",[35,581,582],{"class":142}," None",[35,584,294],{"class":75},[35,586,588,590,592,594,596,598,600,603,605,608,610,613,615],{"class":37,"line":587},29,[35,589,244],{"class":71},[35,591,247],{"class":142},[35,593,250],{"class":75},[35,595,253],{"class":71},[35,597,256],{"class":45},[35,599,201],{"class":200},[35,601,602],{"class":75},"table",[35,604,223],{"class":200},[35,606,607],{"class":45},": no load recorded for ",[35,609,201],{"class":200},[35,611,612],{"class":75},"business_date",[35,614,223],{"class":200},[35,616,514],{"class":45},[35,618,620,623,625],{"class":37,"line":619},30,[35,621,622],{"class":75},"    status, rows ",[35,624,139],{"class":71},[35,626,627],{"class":75}," row\n",[35,629,631,633,636,638,641,644,647,650,652,655,657,659,661,663,665,668,670,673,675,677,679,682,684,686],{"class":37,"line":630},31,[35,632,388],{"class":71},[35,634,635],{"class":75}," (status ",[35,637,288],{"class":71},[35,639,640],{"class":45}," \"complete\"",[35,642,643],{"class":71}," and",[35,645,646],{"class":75}," rows ",[35,648,649],{"class":71},">",[35,651,291],{"class":142},[35,653,654],{"class":75},"), ",[35,656,253],{"class":71},[35,658,256],{"class":45},[35,660,201],{"class":200},[35,662,602],{"class":75},[35,664,223],{"class":200},[35,666,667],{"class":45},": ",[35,669,201],{"class":200},[35,671,672],{"class":75},"status",[35,674,223],{"class":200},[35,676,250],{"class":45},[35,678,201],{"class":200},[35,680,681],{"class":75},"rows",[35,683,414],{"class":71},[35,685,223],{"class":200},[35,687,688],{"class":45}," rows\"\n",[10,690,691,692,696],{},"Each check returns two things: whether the input is ready and a sentence saying why or why not. The sentence is what turns a late-data notice from \"inputs not ready\" into something a person can act on, such as \"sales_20261005.xlsx changed 40s ago, may still be copying\". The checks are specific to the ",[693,694,695],"em",{},"business date"," the report is for, not to \"the newest file\" — picking up the newest file is exactly how a report ends up running on yesterday's data without anyone noticing.",[10,698,699,700,703],{},"The file check has a subtlety. A file that exists may still be arriving: a large export copied over a network share appears at its full name long before the last byte is written. Requiring the file to be unchanged for a couple of minutes is a cheap guard. A marker file is better when the upstream system can write one — the producer writes the data, then creates an empty ",[32,701,702],{},".done"," file, and the marker's existence means the data is complete. For a database, the equivalent is a load-status table that the load process updates at the end, so a half-finished load is never mistaken for a finished one.",[705,706,714,715,714,719,714,723,714,731,714,740,714,744,714,751,714,755,714,759,714,763,714,767,714,771,714,776,714,781,714,785,714,787,714,790,714,792,714,795,714,798,714,802,714,804,714,808,714,810,714,813,714,815,714,818,714,821,714,825,714,827,714,830,714,832,714,835,714,837,714,840,714,843,714,847,714,849,714,852,714,854,714,857,714,859],"svg",{"viewBox":707,"role":708,"ariaLabelledBy":709,"xmlns":712,"style":713},"71 0 618 168","img",[710,711],"up-checks-t","up-checks-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:618px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[716,717,718],"title",{"id":710},"Three kinds of readiness check",[720,721,722],"desc",{"id":711},"A file input is ready when the file for the business date exists, is not empty and has not changed for two minutes. A marker input is ready when the producer has written its .done file. A database input is ready when the load-status table records a complete load with rows for the business date.",[724,725],"rect",{"x":726,"y":727,"width":728,"height":729,"fill":730},"71","0","618","168","#ffffff",[724,732],{"x":733,"y":734,"width":735,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"87.4","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[724,741],{"x":742,"y":734,"width":743,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"119.4","116.0",[745,746,750],"text",{"x":747,"y":748,"style":749},"177.4","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[724,752],{"x":753,"y":734,"width":754,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"235.4","216.3",[745,756,758],{"x":757,"y":748,"style":749},"343.5","B",[724,760],{"x":761,"y":734,"width":762,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"451.7","220.9",[745,764,766],{"x":765,"y":748,"style":749},"562.1","C",[724,768],{"x":733,"y":769,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"40.0","28.0",[745,772,775],{"x":773,"y":774,"style":749},"103.4","59.0","1",[724,777],{"x":742,"y":769,"width":743,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[745,782,784],{"x":747,"y":774,"style":783},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Input",[724,786],{"x":753,"y":769,"width":754,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,788,789],{"x":757,"y":774,"style":783},"Ready when",[724,791],{"x":761,"y":769,"width":762,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,793,794],{"x":765,"y":774,"style":783},"Guards against",[724,796],{"x":733,"y":797,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"68.0",[745,799,801],{"x":773,"y":800,"style":749},"87.0","2",[724,803],{"x":742,"y":797,"width":743,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,805,807],{"x":747,"y":800,"style":806},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Export file",[724,809],{"x":753,"y":797,"width":754,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,811,812],{"x":757,"y":800,"style":806},"dated file exists, stable 2 min",[724,814],{"x":761,"y":797,"width":762,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,816,817],{"x":765,"y":800,"style":806},"yesterday's file, half-copied file",[724,819],{"x":733,"y":820,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"96.0",[745,822,824],{"x":773,"y":823,"style":749},"115.0","3",[724,826],{"x":742,"y":820,"width":743,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,828,829],{"x":747,"y":823,"style":806},"Marker file",[724,831],{"x":753,"y":820,"width":754,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,833,834],{"x":757,"y":823,"style":806},".done written by the producer",[724,836],{"x":761,"y":820,"width":762,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,838,839],{"x":765,"y":823,"style":806},"data still being written",[724,841],{"x":733,"y":842,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"124.0",[745,844,846],{"x":773,"y":845,"style":749},"143.0","4",[724,848],{"x":742,"y":842,"width":743,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,850,851],{"x":747,"y":845,"style":806},"Database table",[724,853],{"x":753,"y":842,"width":754,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,855,856],{"x":757,"y":845,"style":806},"load_status complete, rows > 0",[724,858],{"x":761,"y":842,"width":762,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,860,861],{"x":765,"y":845,"style":806},"failed or partial load",[20,863,865],{"id":864},"step-2-poll-until-ready-or-until-the-deadline","Step 2 — Poll until ready or until the deadline",[25,867,869],{"className":62,"code":868,"language":64,"meta":30,"style":30},"import time\n\ndef wait_until_ready(checks, deadline, now=datetime.now, sleep=time.sleep, first_wait=60, max_wait=600):\n    wait = first_wait\n    while True:\n        results = [(name, *check()) for name, check in checks]\n        pending = [(n, why) for n, ok, why in results if not ok]\n        if not pending:\n            return True, results\n        if now() >= deadline:\n            return False, results\n        pause = min(wait, max_wait, max((deadline - now()).total_seconds(), 0))\n        print(f\"{now():%H:%M:%S} waiting {pause:.0f}s for: \" + \"; \".join(why for _, why in pending))\n        sleep(pause)\n        wait *= 2\n",[32,870,871,878,882,917,927,936,964,992,1002,1012,1025,1033,1062,1134,1139],{"__ignoreMap":30},[35,872,873,875],{"class":37,"line":38},[35,874,72],{"class":71},[35,876,877],{"class":75}," time\n",[35,879,880],{"class":37,"line":79},[35,881,123],{"emptyLinePlaceholder":122},[35,883,884,886,889,892,894,897,899,902,904,907,910,912,915],{"class":37,"line":93},[35,885,129],{"class":71},[35,887,888],{"class":132}," wait_until_ready",[35,890,891],{"class":75},"(checks, deadline, now",[35,893,139],{"class":71},[35,895,896],{"class":75},"datetime.now, sleep",[35,898,139],{"class":71},[35,900,901],{"class":75},"time.sleep, first_wait",[35,903,139],{"class":71},[35,905,906],{"class":142},"60",[35,908,909],{"class":75},", max_wait",[35,911,139],{"class":71},[35,913,914],{"class":142},"600",[35,916,154],{"class":75},[35,918,919,922,924],{"class":37,"line":106},[35,920,921],{"class":75},"    wait ",[35,923,139],{"class":71},[35,925,926],{"class":75}," first_wait\n",[35,928,929,932,934],{"class":37,"line":119},[35,930,931],{"class":71},"    while",[35,933,391],{"class":142},[35,935,294],{"class":75},[35,937,938,941,943,946,949,952,955,958,961],{"class":37,"line":126},[35,939,940],{"class":75},"        results ",[35,942,139],{"class":71},[35,944,945],{"class":75}," [(name, ",[35,947,948],{"class":71},"*",[35,950,951],{"class":75},"check()) ",[35,953,954],{"class":71},"for",[35,956,957],{"class":75}," name, check ",[35,959,960],{"class":71},"in",[35,962,963],{"class":75}," checks]\n",[35,965,966,969,971,974,976,979,981,984,987,989],{"class":37,"line":157},[35,967,968],{"class":75},"        pending ",[35,970,139],{"class":71},[35,972,973],{"class":75}," [(n, why) ",[35,975,954],{"class":71},[35,977,978],{"class":75}," n, ok, why ",[35,980,960],{"class":71},[35,982,983],{"class":75}," results ",[35,985,986],{"class":71},"if",[35,988,235],{"class":71},[35,990,991],{"class":75}," ok]\n",[35,993,994,997,999],{"class":37,"line":163},[35,995,996],{"class":71},"        if",[35,998,235],{"class":71},[35,1000,1001],{"class":75}," pending:\n",[35,1003,1004,1007,1009],{"class":37,"line":180},[35,1005,1006],{"class":71},"            return",[35,1008,391],{"class":142},[35,1010,1011],{"class":75},", results\n",[35,1013,1014,1016,1019,1022],{"class":37,"line":229},[35,1015,996],{"class":71},[35,1017,1018],{"class":75}," now() ",[35,1020,1021],{"class":71},">=",[35,1023,1024],{"class":75}," deadline:\n",[35,1026,1027,1029,1031],{"class":37,"line":241},[35,1028,1006],{"class":71},[35,1030,247],{"class":142},[35,1032,1011],{"class":75},[35,1034,1035,1038,1040,1043,1046,1049,1052,1054,1057,1059],{"class":37,"line":269},[35,1036,1037],{"class":75},"        pause ",[35,1039,139],{"class":71},[35,1041,1042],{"class":142}," min",[35,1044,1045],{"class":75},"(wait, max_wait, ",[35,1047,1048],{"class":142},"max",[35,1050,1051],{"class":75},"((deadline ",[35,1053,330],{"class":71},[35,1055,1056],{"class":75}," now()).total_seconds(), ",[35,1058,727],{"class":142},[35,1060,1061],{"class":75},"))\n",[35,1063,1064,1067,1070,1072,1074,1076,1079,1082,1084,1087,1090,1093,1095,1098,1100,1103,1105,1108,1110,1112,1115,1118,1121,1124,1126,1129,1131],{"class":37,"line":280},[35,1065,1066],{"class":142},"        print",[35,1068,1069],{"class":75},"(",[35,1071,253],{"class":71},[35,1073,256],{"class":45},[35,1075,201],{"class":200},[35,1077,1078],{"class":75},"now()",[35,1080,1081],{"class":71},":",[35,1083,207],{"class":75},[35,1085,1086],{"class":71},"H",[35,1088,1089],{"class":75},":%",[35,1091,1092],{"class":71},"M",[35,1094,1089],{"class":75},[35,1096,1097],{"class":71},"S",[35,1099,223],{"class":200},[35,1101,1102],{"class":45}," waiting ",[35,1104,201],{"class":200},[35,1106,1107],{"class":75},"pause",[35,1109,377],{"class":71},[35,1111,223],{"class":200},[35,1113,1114],{"class":45},"s for: \"",[35,1116,1117],{"class":71}," +",[35,1119,1120],{"class":45}," \"; \"",[35,1122,1123],{"class":75},".join(why ",[35,1125,954],{"class":71},[35,1127,1128],{"class":75}," _, why ",[35,1130,960],{"class":71},[35,1132,1133],{"class":75}," pending))\n",[35,1135,1136],{"class":37,"line":297},[35,1137,1138],{"class":75},"        sleep(pause)\n",[35,1140,1141,1144,1147],{"class":37,"line":319},[35,1142,1143],{"class":75},"        wait ",[35,1145,1146],{"class":71},"*=",[35,1148,1149],{"class":142}," 2\n",[10,1151,1152],{},"The loop runs every check, returns as soon as all of them pass, and otherwise sleeps and tries again. The waits grow — one minute, two, four, then capped at ten — so a late input is noticed quickly when it is only a little late, without checking every few seconds for an hour when it is very late. The last wait is shortened so the loop wakes exactly at the deadline. The clock and the sleep function are parameters, which costs nothing in production and makes it possible to test the whole loop in a fraction of a second, as step 4 does.",[20,1154,1156],{"id":1155},"step-3-run-or-say-clearly-that-the-data-is-late","Step 3 — Run, or say clearly that the data is late",[25,1158,1160],{"className":62,"code":1159,"language":64,"meta":30,"style":30},"def run_report(business_date, folder, db, deadline, now=datetime.now, sleep=time.sleep):\n    checks = [(\"export\", lambda: export_file_ready(folder, business_date, now=now())),\n              (\"marker\", lambda: marker_ready(folder, business_date)),\n              (\"orders table\", lambda: table_loaded(db, \"orders\", business_date))]\n    ready, results = wait_until_ready(checks, deadline, now=now, sleep=sleep)\n    if not ready:\n        lines = [f\"- {name}: {'ready' if ok else why}\" for name, ok, why in results]\n        return \"LATE\", (f\"The {business_date:%d %b} sales report has not been produced: \"\n                        f\"inputs were not ready by {deadline:%H:%M}.\\n\" + \"\\n\".join(lines))\n    # build_report(business_date) would go here\n    return \"OK\", f\"report for {business_date} built from: \" + \"; \".join(why for _, _, why in results)\n",[32,1161,1162,1181,1211,1226,1246,1271,1280,1334,1368,1411,1417],{"__ignoreMap":30},[35,1163,1164,1166,1169,1172,1174,1176,1178],{"class":37,"line":38},[35,1165,129],{"class":71},[35,1167,1168],{"class":132}," run_report",[35,1170,1171],{"class":75},"(business_date, folder, db, deadline, now",[35,1173,139],{"class":71},[35,1175,896],{"class":75},[35,1177,139],{"class":71},[35,1179,1180],{"class":75},"time.sleep):\n",[35,1182,1183,1186,1188,1191,1194,1196,1199,1202,1206,1208],{"class":37,"line":79},[35,1184,1185],{"class":75},"    checks ",[35,1187,139],{"class":71},[35,1189,1190],{"class":75}," [(",[35,1192,1193],{"class":45},"\"export\"",[35,1195,250],{"class":75},[35,1197,1198],{"class":71},"lambda",[35,1200,1201],{"class":75},": export_file_ready(folder, business_date, ",[35,1203,1205],{"class":1204},"sa561","now",[35,1207,139],{"class":71},[35,1209,1210],{"class":75},"now())),\n",[35,1212,1213,1216,1219,1221,1223],{"class":37,"line":93},[35,1214,1215],{"class":75},"              (",[35,1217,1218],{"class":45},"\"marker\"",[35,1220,250],{"class":75},[35,1222,1198],{"class":71},[35,1224,1225],{"class":75},": marker_ready(folder, business_date)),\n",[35,1227,1228,1230,1233,1235,1237,1240,1243],{"class":37,"line":106},[35,1229,1215],{"class":75},[35,1231,1232],{"class":45},"\"orders table\"",[35,1234,250],{"class":75},[35,1236,1198],{"class":71},[35,1238,1239],{"class":75},": table_loaded(db, ",[35,1241,1242],{"class":45},"\"orders\"",[35,1244,1245],{"class":75},", business_date))]\n",[35,1247,1248,1251,1253,1256,1258,1260,1263,1266,1268],{"class":37,"line":119},[35,1249,1250],{"class":75},"    ready, results ",[35,1252,139],{"class":71},[35,1254,1255],{"class":75}," wait_until_ready(checks, deadline, ",[35,1257,1205],{"class":1204},[35,1259,139],{"class":71},[35,1261,1262],{"class":75},"now, ",[35,1264,1265],{"class":1204},"sleep",[35,1267,139],{"class":71},[35,1269,1270],{"class":75},"sleep)\n",[35,1272,1273,1275,1277],{"class":37,"line":126},[35,1274,232],{"class":71},[35,1276,235],{"class":71},[35,1278,1279],{"class":75}," ready:\n",[35,1281,1282,1285,1287,1290,1292,1295,1297,1300,1302,1304,1306,1309,1311,1314,1316,1319,1321,1323,1326,1329,1331],{"class":37,"line":157},[35,1283,1284],{"class":75},"        lines ",[35,1286,139],{"class":71},[35,1288,1289],{"class":75}," [",[35,1291,253],{"class":71},[35,1293,1294],{"class":45},"\"- ",[35,1296,201],{"class":200},[35,1298,1299],{"class":75},"name",[35,1301,223],{"class":200},[35,1303,667],{"class":45},[35,1305,201],{"class":200},[35,1307,1308],{"class":45},"'ready'",[35,1310,500],{"class":71},[35,1312,1313],{"class":75}," ok ",[35,1315,506],{"class":71},[35,1317,1318],{"class":75}," why",[35,1320,223],{"class":200},[35,1322,256],{"class":45},[35,1324,1325],{"class":71}," for",[35,1327,1328],{"class":75}," name, ok, why ",[35,1330,960],{"class":71},[35,1332,1333],{"class":75}," results]\n",[35,1335,1336,1338,1341,1344,1346,1349,1351,1353,1355,1358,1360,1363,1365],{"class":37,"line":163},[35,1337,244],{"class":71},[35,1339,1340],{"class":45}," \"LATE\"",[35,1342,1343],{"class":75},", (",[35,1345,253],{"class":71},[35,1347,1348],{"class":45},"\"The ",[35,1350,201],{"class":200},[35,1352,204],{"class":75},[35,1354,207],{"class":71},[35,1356,1357],{"class":75},"d ",[35,1359,207],{"class":71},[35,1361,1362],{"class":75},"b",[35,1364,223],{"class":200},[35,1366,1367],{"class":45}," sales report has not been produced: \"\n",[35,1369,1370,1373,1376,1378,1381,1383,1386,1388,1390,1392,1394,1397,1399,1401,1404,1406,1408],{"class":37,"line":180},[35,1371,1372],{"class":71},"                        f",[35,1374,1375],{"class":45},"\"inputs were not ready by ",[35,1377,201],{"class":200},[35,1379,1380],{"class":75},"deadline:",[35,1382,207],{"class":71},[35,1384,1385],{"class":75},"H:",[35,1387,207],{"class":71},[35,1389,1092],{"class":75},[35,1391,223],{"class":200},[35,1393,18],{"class":45},[35,1395,1396],{"class":200},"\\n",[35,1398,256],{"class":45},[35,1400,1117],{"class":71},[35,1402,1403],{"class":45}," \"",[35,1405,1396],{"class":200},[35,1407,256],{"class":45},[35,1409,1410],{"class":75},".join(lines))\n",[35,1412,1413],{"class":37,"line":229},[35,1414,1416],{"class":1415},"s-wDw","    # build_report(business_date) would go here\n",[35,1418,1419,1421,1424,1426,1428,1431,1433,1435,1437,1440,1442,1444,1446,1448,1451,1453],{"class":37,"line":241},[35,1420,388],{"class":71},[35,1422,1423],{"class":45}," \"OK\"",[35,1425,250],{"class":75},[35,1427,253],{"class":71},[35,1429,1430],{"class":45},"\"report for ",[35,1432,201],{"class":200},[35,1434,612],{"class":75},[35,1436,223],{"class":200},[35,1438,1439],{"class":45}," built from: \"",[35,1441,1117],{"class":71},[35,1443,1120],{"class":45},[35,1445,1123],{"class":75},[35,1447,954],{"class":71},[35,1449,1450],{"class":75}," _, _, why ",[35,1452,960],{"class":71},[35,1454,1455],{"class":75}," results)\n",[10,1457,1458,1459,18],{},"When the deadline passes, the job does not fall back to the data it has. It sends a short notice instead — to the report's recipients if they would otherwise wonder where it is, or to the people who run the upstream system — listing each input with its state. A notice that says \"the orders table load is still running\" saves an hour of emails; a report built on partial data costs much more, because nobody knows it is wrong. Once the data arrives, the report can be produced by rerunning the job for that business date, which is safe if the job is written to be rerun, as described in ",[14,1460,1462],{"href":1461},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun\u002F","Make an Excel Report Job Safe to Rerun",[705,1464,714,1470,714,1473,714,1476,714,1480,714,1487,714,1493,714,1497,714,1501,714,1506,714,1511,714,1514,714,1520,714,1523,714,1527,714,1529,714,1533,714,1536,714,1541,714,1546,714,1549,714,1552,714,1556,714,1559,714,1564,714,1569,714,1572,714,1576,714,1579,714,1584,714,1589,714,1593],{"viewBox":1465,"role":708,"ariaLabelledBy":1466,"xmlns":712,"style":1469},"4 0 752 126",[1467,1468],"up-flow-t","up-flow-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[716,1471,1472],{"id":1467},"What the job does at 07:00",[720,1474,1475],{"id":1468},"The job runs all readiness checks. If everything is ready it builds the report. If not, it waits with growing intervals and checks again until the deadline, after which it sends a data-is-late notice listing each input's state instead of building a report.",[724,1477],{"x":846,"y":727,"width":1478,"height":1479,"fill":730},"752","126",[724,1481],{"x":1482,"y":734,"width":1483,"height":1484,"rx":1485,"fill":737,"stroke":738,"style":1486},"20.0","115.2","94.0","12","stroke-width:2px",[745,1488,1492],{"x":1489,"y":1490,"style":1491},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","07:00 start",[745,1494,695],{"x":1489,"y":1495,"style":1496},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[745,1498,1500],{"x":1489,"y":1499,"style":1496},"87.2","known",[37,1502],{"x1":1503,"y1":1504,"x2":1505,"y2":1504,"stroke":779,"style":1486},"140.2","63.0","162.2",[1507,1508],"polygon",{"points":1509,"fill":1510},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[724,1512],{"x":1513,"y":734,"width":1483,"height":1484,"rx":1485,"fill":778,"stroke":779,"style":1486},"171.2",[745,1515,1519],{"x":1516,"y":1517,"style":1518},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Run all",[745,1521,1522],{"x":1516,"y":774,"style":1518},"checks",[745,1524,1526],{"x":1516,"y":1525,"style":1496},"78.5","file, marker,",[745,1528,602],{"x":1516,"y":820,"style":1496},[37,1530],{"x1":1531,"y1":1504,"x2":1532,"y2":1504,"stroke":779,"style":1486},"291.4","313.4",[1507,1534],{"points":1535,"fill":1510},"317.4,63.0 308.4,68.0 308.4,58.0",[724,1537],{"x":1538,"y":734,"width":1483,"height":1484,"rx":1485,"fill":1539,"stroke":1540,"style":1486},"322.4","#fdefd8","var(--gold,#b4740a)",[745,1542,1545],{"x":1543,"y":1490,"style":1544},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Not ready?",[745,1547,1548],{"x":1543,"y":1495,"style":1496},"wait 1, 2, 4…",[745,1550,1551],{"x":1543,"y":1499,"style":1496},"min",[37,1553],{"x1":1554,"y1":1504,"x2":1555,"y2":1504,"stroke":779,"style":1486},"442.6","464.6",[1507,1557],{"points":1558,"fill":1510},"468.6,63.0 459.6,68.0 459.6,58.0",[724,1560],{"x":1561,"y":734,"width":1483,"height":1484,"rx":1485,"fill":1562,"stroke":1563,"style":1486},"473.6","#d9f4f1","var(--teal,#0f9488)",[745,1565,1568],{"x":1566,"y":774,"style":1567},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Ready",[745,1570,1571],{"x":1566,"y":1525,"style":1496},"build and send",[37,1573],{"x1":1574,"y1":1504,"x2":1575,"y2":1504,"stroke":779,"style":1486},"593.8","615.8",[1507,1577],{"points":1578,"fill":1510},"619.8,63.0 610.8,68.0 610.8,58.0",[724,1580],{"x":1581,"y":734,"width":1483,"height":1484,"rx":1485,"fill":1582,"stroke":1583,"style":1486},"624.8","#fce9e9","var(--danger,#dc2626)",[745,1585,1588],{"x":1586,"y":1490,"style":1587},"682.4","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Deadline",[745,1590,1592],{"x":1586,"y":1591,"style":1587},"67.8","passed",[745,1594,1595],{"x":1586,"y":1499,"style":1496},"late-data notice",[20,1597,1599],{"id":1598},"step-4-test-it-against-a-simulated-clock","Step 4 — Test it against a simulated clock",[25,1601,1603],{"className":62,"code":1602,"language":64,"meta":30,"style":30},"import os, tempfile\n\nclass FakeClock:\n    def __init__(self, start):\n        self.t = start\n    def now(self):\n        return self.t\n    def sleep(self, seconds):\n        self.t += timedelta(seconds=seconds)\n        for at, action in list(events):\n            if at \u003C= self.t:\n                action(); events.remove((at, action))\n\nfolder = Path(tempfile.mkdtemp()); db = str(folder \u002F \"warehouse.sqlite\")\nwith closing(sqlite3.connect(db)) as con, con:\n    con.execute(\"CREATE TABLE load_status (table_name TEXT, business_date TEXT, status TEXT, rows INTEGER)\")\nbd = date(2026, 10, 5)\n\ndef drop_export(at):\n    p = folder \u002F f\"sales_{bd:%Y%m%d}.xlsx\"\n    p.write_bytes(b\"x\" * 48_213)\n    os.utime(p, (at.timestamp(), at.timestamp()))\ndef write_marker():\n    (folder \u002F f\"sales_{bd:%Y%m%d}.done\").touch()\ndef finish_load():\n    with closing(sqlite3.connect(db)) as con, con:\n        con.execute(\"INSERT INTO load_status VALUES ('orders', ?, 'complete', 18342)\", (bd.isoformat(),))\n\nstart = datetime(2026, 10, 6, 7, 0)\nclock = FakeClock(start)\nevents = [(start + timedelta(minutes=5), lambda: drop_export(start + timedelta(minutes=5))),\n          (start + timedelta(minutes=6), write_marker), (start + timedelta(minutes=13), finish_load)]\nprint(run_report(bd, folder, db, deadline=start + timedelta(minutes=45), now=clock.now, sleep=clock.sleep))\n",[32,1604,1605,1612,1616,1626,1637,1650,1660,1670,1680,1700,1716,1732,1737,1741,1767,1779,1789,1814,1818,1828,1865,1883,1888,1898,1933,1942,1952,1963,1967,1999,2009,2051,2084],{"__ignoreMap":30},[35,1606,1607,1609],{"class":37,"line":38},[35,1608,72],{"class":71},[35,1610,1611],{"class":75}," os, tempfile\n",[35,1613,1614],{"class":37,"line":79},[35,1615,123],{"emptyLinePlaceholder":122},[35,1617,1618,1621,1624],{"class":37,"line":93},[35,1619,1620],{"class":71},"class",[35,1622,1623],{"class":41}," FakeClock",[35,1625,294],{"class":75},[35,1627,1628,1631,1634],{"class":37,"line":106},[35,1629,1630],{"class":71},"    def",[35,1632,1633],{"class":142}," __init__",[35,1635,1636],{"class":75},"(self, start):\n",[35,1638,1639,1642,1645,1647],{"class":37,"line":119},[35,1640,1641],{"class":142},"        self",[35,1643,1644],{"class":75},".t ",[35,1646,139],{"class":71},[35,1648,1649],{"class":75}," start\n",[35,1651,1652,1654,1657],{"class":37,"line":126},[35,1653,1630],{"class":71},[35,1655,1656],{"class":132}," now",[35,1658,1659],{"class":75},"(self):\n",[35,1661,1662,1664,1667],{"class":37,"line":157},[35,1663,244],{"class":71},[35,1665,1666],{"class":142}," self",[35,1668,1669],{"class":75},".t\n",[35,1671,1672,1674,1677],{"class":37,"line":163},[35,1673,1630],{"class":71},[35,1675,1676],{"class":132}," sleep",[35,1678,1679],{"class":75},"(self, seconds):\n",[35,1681,1682,1684,1686,1689,1692,1695,1697],{"class":37,"line":180},[35,1683,1641],{"class":142},[35,1685,1644],{"class":75},[35,1687,1688],{"class":71},"+=",[35,1690,1691],{"class":75}," timedelta(",[35,1693,1694],{"class":1204},"seconds",[35,1696,139],{"class":71},[35,1698,1699],{"class":75},"seconds)\n",[35,1701,1702,1705,1708,1710,1713],{"class":37,"line":229},[35,1703,1704],{"class":71},"        for",[35,1706,1707],{"class":75}," at, action ",[35,1709,960],{"class":71},[35,1711,1712],{"class":142}," list",[35,1714,1715],{"class":75},"(events):\n",[35,1717,1718,1721,1724,1727,1729],{"class":37,"line":241},[35,1719,1720],{"class":71},"            if",[35,1722,1723],{"class":75}," at ",[35,1725,1726],{"class":71},"\u003C=",[35,1728,1666],{"class":142},[35,1730,1731],{"class":75},".t:\n",[35,1733,1734],{"class":37,"line":269},[35,1735,1736],{"class":75},"                action(); events.remove((at, action))\n",[35,1738,1739],{"class":37,"line":280},[35,1740,123],{"emptyLinePlaceholder":122},[35,1742,1743,1746,1748,1751,1753,1756,1759,1761,1764],{"class":37,"line":297},[35,1744,1745],{"class":75},"folder ",[35,1747,139],{"class":71},[35,1749,1750],{"class":75}," Path(tempfile.mkdtemp()); db ",[35,1752,139],{"class":71},[35,1754,1755],{"class":142}," str",[35,1757,1758],{"class":75},"(folder ",[35,1760,191],{"class":71},[35,1762,1763],{"class":45}," \"warehouse.sqlite\"",[35,1765,1766],{"class":75},")\n",[35,1768,1769,1772,1774,1776],{"class":37,"line":319},[35,1770,1771],{"class":71},"with",[35,1773,539],{"class":75},[35,1775,542],{"class":71},[35,1777,1778],{"class":75}," con, con:\n",[35,1780,1781,1784,1787],{"class":37,"line":336},[35,1782,1783],{"class":75},"    con.execute(",[35,1785,1786],{"class":45},"\"CREATE TABLE load_status (table_name TEXT, business_date TEXT, status TEXT, rows INTEGER)\"",[35,1788,1766],{"class":75},[35,1790,1791,1794,1796,1799,1802,1804,1807,1809,1812],{"class":37,"line":350},[35,1792,1793],{"class":75},"bd ",[35,1795,139],{"class":71},[35,1797,1798],{"class":75}," date(",[35,1800,1801],{"class":142},"2026",[35,1803,250],{"class":75},[35,1805,1806],{"class":142},"10",[35,1808,250],{"class":75},[35,1810,1811],{"class":142},"5",[35,1813,1766],{"class":75},[35,1815,1816],{"class":37,"line":385},[35,1817,123],{"emptyLinePlaceholder":122},[35,1819,1820,1822,1825],{"class":37,"line":422},[35,1821,129],{"class":71},[35,1823,1824],{"class":132}," drop_export",[35,1826,1827],{"class":75},"(at):\n",[35,1829,1830,1833,1835,1838,1840,1842,1844,1846,1849,1851,1853,1855,1857,1859,1861,1863],{"class":37,"line":427},[35,1831,1832],{"class":75},"    p ",[35,1834,139],{"class":71},[35,1836,1837],{"class":75}," folder ",[35,1839,191],{"class":71},[35,1841,194],{"class":71},[35,1843,197],{"class":45},[35,1845,201],{"class":200},[35,1847,1848],{"class":75},"bd:",[35,1850,207],{"class":71},[35,1852,210],{"class":75},[35,1854,207],{"class":71},[35,1856,215],{"class":75},[35,1858,207],{"class":71},[35,1860,220],{"class":75},[35,1862,223],{"class":200},[35,1864,226],{"class":45},[35,1866,1867,1870,1872,1875,1878,1881],{"class":37,"line":438},[35,1868,1869],{"class":75},"    p.write_bytes(",[35,1871,1362],{"class":71},[35,1873,1874],{"class":45},"\"x\"",[35,1876,1877],{"class":71}," *",[35,1879,1880],{"class":142}," 48_213",[35,1882,1766],{"class":75},[35,1884,1885],{"class":37,"line":475},[35,1886,1887],{"class":75},"    os.utime(p, (at.timestamp(), at.timestamp()))\n",[35,1889,1890,1892,1895],{"class":37,"line":517},[35,1891,129],{"class":71},[35,1893,1894],{"class":132}," write_marker",[35,1896,1897],{"class":75},"():\n",[35,1899,1900,1903,1905,1907,1909,1911,1913,1915,1917,1919,1921,1923,1925,1927,1930],{"class":37,"line":522},[35,1901,1902],{"class":75},"    (folder ",[35,1904,191],{"class":71},[35,1906,194],{"class":71},[35,1908,197],{"class":45},[35,1910,201],{"class":200},[35,1912,1848],{"class":75},[35,1914,207],{"class":71},[35,1916,210],{"class":75},[35,1918,207],{"class":71},[35,1920,215],{"class":75},[35,1922,207],{"class":71},[35,1924,220],{"class":75},[35,1926,223],{"class":200},[35,1928,1929],{"class":45},".done\"",[35,1931,1932],{"class":75},").touch()\n",[35,1934,1935,1937,1940],{"class":37,"line":533},[35,1936,129],{"class":71},[35,1938,1939],{"class":132}," finish_load",[35,1941,1897],{"class":75},[35,1943,1944,1946,1948,1950],{"class":37,"line":548},[35,1945,536],{"class":71},[35,1947,539],{"class":75},[35,1949,542],{"class":71},[35,1951,1778],{"class":75},[35,1953,1954,1957,1960],{"class":37,"line":565},[35,1955,1956],{"class":75},"        con.execute(",[35,1958,1959],{"class":45},"\"INSERT INTO load_status VALUES ('orders', ?, 'complete', 18342)\"",[35,1961,1962],{"class":75},", (bd.isoformat(),))\n",[35,1964,1965],{"class":37,"line":571},[35,1966,123],{"emptyLinePlaceholder":122},[35,1968,1969,1972,1974,1977,1979,1981,1983,1985,1988,1990,1993,1995,1997],{"class":37,"line":587},[35,1970,1971],{"class":75},"start ",[35,1973,139],{"class":71},[35,1975,1976],{"class":75}," datetime(",[35,1978,1801],{"class":142},[35,1980,250],{"class":75},[35,1982,1806],{"class":142},[35,1984,250],{"class":75},[35,1986,1987],{"class":142},"6",[35,1989,250],{"class":75},[35,1991,1992],{"class":142},"7",[35,1994,250],{"class":75},[35,1996,727],{"class":142},[35,1998,1766],{"class":75},[35,2000,2001,2004,2006],{"class":37,"line":619},[35,2002,2003],{"class":75},"clock ",[35,2005,139],{"class":71},[35,2007,2008],{"class":75}," FakeClock(start)\n",[35,2010,2011,2014,2016,2019,2022,2024,2027,2029,2031,2033,2035,2038,2040,2042,2044,2046,2048],{"class":37,"line":630},[35,2012,2013],{"class":75},"events ",[35,2015,139],{"class":71},[35,2017,2018],{"class":75}," [(start ",[35,2020,2021],{"class":71},"+",[35,2023,1691],{"class":75},[35,2025,2026],{"class":1204},"minutes",[35,2028,139],{"class":71},[35,2030,1811],{"class":142},[35,2032,654],{"class":75},[35,2034,1198],{"class":71},[35,2036,2037],{"class":75},": drop_export(start ",[35,2039,2021],{"class":71},[35,2041,1691],{"class":75},[35,2043,2026],{"class":1204},[35,2045,139],{"class":71},[35,2047,1811],{"class":142},[35,2049,2050],{"class":75},"))),\n",[35,2052,2054,2057,2059,2061,2063,2065,2067,2070,2072,2074,2076,2078,2081],{"class":37,"line":2053},32,[35,2055,2056],{"class":75},"          (start ",[35,2058,2021],{"class":71},[35,2060,1691],{"class":75},[35,2062,2026],{"class":1204},[35,2064,139],{"class":71},[35,2066,1987],{"class":142},[35,2068,2069],{"class":75},"), write_marker), (start ",[35,2071,2021],{"class":71},[35,2073,1691],{"class":75},[35,2075,2026],{"class":1204},[35,2077,139],{"class":71},[35,2079,2080],{"class":142},"13",[35,2082,2083],{"class":75},"), finish_load)]\n",[35,2085,2087,2090,2093,2096,2098,2100,2102,2104,2106,2108,2111,2113,2115,2117,2120,2122,2124],{"class":37,"line":2086},33,[35,2088,2089],{"class":142},"print",[35,2091,2092],{"class":75},"(run_report(bd, folder, db, ",[35,2094,2095],{"class":1204},"deadline",[35,2097,139],{"class":71},[35,2099,1971],{"class":75},[35,2101,2021],{"class":71},[35,2103,1691],{"class":75},[35,2105,2026],{"class":1204},[35,2107,139],{"class":71},[35,2109,2110],{"class":142},"45",[35,2112,654],{"class":75},[35,2114,1205],{"class":1204},[35,2116,139],{"class":71},[35,2118,2119],{"class":75},"clock.now, ",[35,2121,1265],{"class":1204},[35,2123,139],{"class":71},[35,2125,2126],{"class":75},"clock.sleep))\n",[10,2128,2129,2130,2133,2134,2137],{},"The fake clock replaces ",[32,2131,2132],{},"datetime.now"," and ",[32,2135,2136],{},"time.sleep",": sleeping simply moves the clock forward and fires any events that are due, such as the export file landing at 07:05 or the database load finishing at 07:13. The test runs in milliseconds but walks through the real sequence of waits. It is worth also running it with an event that never happens — the database load never finishing — to see the late-data notice, which is what the next block does.",[25,2139,2141],{"className":62,"code":2140,"language":64,"meta":30,"style":30},"for f in folder.iterdir():\n    if f.suffix in (\".xlsx\", \".done\"):\n        f.unlink()\nwith closing(sqlite3.connect(db)) as con, con:\n    con.execute(\"DELETE FROM load_status\")\nclock = FakeClock(start)\nevents = [(start + timedelta(minutes=5), lambda: drop_export(start + timedelta(minutes=5))),\n          (start + timedelta(minutes=6), write_marker)]\nstatus, message = run_report(bd, folder, db, deadline=start + timedelta(minutes=45), now=clock.now, sleep=clock.sleep)\nprint(status); print(message)\n",[32,2142,2143,2155,2177,2182,2192,2201,2209,2245,2262,2303],{"__ignoreMap":30},[35,2144,2145,2147,2150,2152],{"class":37,"line":38},[35,2146,954],{"class":71},[35,2148,2149],{"class":75}," f ",[35,2151,960],{"class":71},[35,2153,2154],{"class":75}," folder.iterdir():\n",[35,2156,2157,2159,2162,2164,2167,2170,2172,2175],{"class":37,"line":79},[35,2158,232],{"class":71},[35,2160,2161],{"class":75}," f.suffix ",[35,2163,960],{"class":71},[35,2165,2166],{"class":75}," (",[35,2168,2169],{"class":45},"\".xlsx\"",[35,2171,250],{"class":75},[35,2173,2174],{"class":45},"\".done\"",[35,2176,154],{"class":75},[35,2178,2179],{"class":37,"line":93},[35,2180,2181],{"class":75},"        f.unlink()\n",[35,2183,2184,2186,2188,2190],{"class":37,"line":106},[35,2185,1771],{"class":71},[35,2187,539],{"class":75},[35,2189,542],{"class":71},[35,2191,1778],{"class":75},[35,2193,2194,2196,2199],{"class":37,"line":119},[35,2195,1783],{"class":75},[35,2197,2198],{"class":45},"\"DELETE FROM load_status\"",[35,2200,1766],{"class":75},[35,2202,2203,2205,2207],{"class":37,"line":126},[35,2204,2003],{"class":75},[35,2206,139],{"class":71},[35,2208,2008],{"class":75},[35,2210,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233,2235,2237,2239,2241,2243],{"class":37,"line":157},[35,2212,2013],{"class":75},[35,2214,139],{"class":71},[35,2216,2018],{"class":75},[35,2218,2021],{"class":71},[35,2220,1691],{"class":75},[35,2222,2026],{"class":1204},[35,2224,139],{"class":71},[35,2226,1811],{"class":142},[35,2228,654],{"class":75},[35,2230,1198],{"class":71},[35,2232,2037],{"class":75},[35,2234,2021],{"class":71},[35,2236,1691],{"class":75},[35,2238,2026],{"class":1204},[35,2240,139],{"class":71},[35,2242,1811],{"class":142},[35,2244,2050],{"class":75},[35,2246,2247,2249,2251,2253,2255,2257,2259],{"class":37,"line":163},[35,2248,2056],{"class":75},[35,2250,2021],{"class":71},[35,2252,1691],{"class":75},[35,2254,2026],{"class":1204},[35,2256,139],{"class":71},[35,2258,1987],{"class":142},[35,2260,2261],{"class":75},"), write_marker)]\n",[35,2263,2264,2267,2269,2272,2274,2276,2278,2280,2282,2284,2286,2288,2290,2292,2294,2296,2298,2300],{"class":37,"line":180},[35,2265,2266],{"class":75},"status, message ",[35,2268,139],{"class":71},[35,2270,2271],{"class":75}," run_report(bd, folder, db, ",[35,2273,2095],{"class":1204},[35,2275,139],{"class":71},[35,2277,1971],{"class":75},[35,2279,2021],{"class":71},[35,2281,1691],{"class":75},[35,2283,2026],{"class":1204},[35,2285,139],{"class":71},[35,2287,2110],{"class":142},[35,2289,654],{"class":75},[35,2291,1205],{"class":1204},[35,2293,139],{"class":71},[35,2295,2119],{"class":75},[35,2297,1265],{"class":1204},[35,2299,139],{"class":71},[35,2301,2302],{"class":75},"clock.sleep)\n",[35,2304,2305,2307,2310,2312],{"class":37,"line":229},[35,2306,2089],{"class":142},[35,2308,2309],{"class":75},"(status); ",[35,2311,2089],{"class":142},[35,2313,2314],{"class":75},"(message)\n",[10,2316,2317],{},"In the first run the checks at 07:00, 07:01 and 07:03 find nothing; at 07:07 the export and marker are ready and only the database load is outstanding; at 07:15 everything passes and the report is built. In the second run the load never finishes, the waits grow to the ten-minute cap, and at 07:45 the job returns the late notice with the export and marker marked ready and the orders table named as the problem.",[705,2319,714,2325,714,2328,714,2331,714,2336,714,2339,714,2343,714,2346,714,2350,714,2353,714,2357,714,2360,714,2364,714,2368,714,2372,714,2376,714,2378,714,2381,714,2383,714,2386,714,2388,714,2390,714,2392,714,2395,714,2397,714,2400,714,2402,714,2405,714,2407,714,2409,714,2411,714,2414,714,2416,714,2419,714,2421,714,2423,714,2425,714,2428,714,2430,714,2433,714,2435,714,2437,714,2439,714,2442,714,2444,714,2446,714,2448,714,2450,714,2452,714,2454,714,2456,714,2459,714,2461,714,2463,714,2465,714,2468,714,2470,714,2472,714,2474,714,2476,714,2478,714,2480,714,2482,714,2485,714,2488,714,2491,714,2493,714,2496,714,2498,714,2501,714,2503,714,2505,714,2507,714,2509,714,2511,714,2514,714,2517,714,2520,714,2522,714,2525,714,2527,714,2529,714,2531,714,2533,714,2535,714,2538,714,2540],{"viewBox":2320,"role":708,"ariaLabelledBy":2321,"xmlns":712,"style":2324},"99 0 562 224",[2322,2323],"up-timeline-t","up-timeline-d","width:100%;max-width:562px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[716,2326,2327],{"id":2322},"The simulated morning",[720,2329,2330],{"id":2323},"Checks at 07:00, 07:01 and 07:03 find no inputs. The export lands at 07:05 and the marker at 07:06, so the 07:07 check waits only for the database. The load completes at 07:13 and the 07:15 check builds the report.",[724,2332],{"x":2333,"y":727,"width":2334,"height":2335,"fill":730},"99","562","224",[724,2337],{"x":2338,"y":734,"width":735,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"115.1",[724,2340],{"x":2341,"y":734,"width":2342,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"147.1","78.3",[745,2344,750],{"x":2345,"y":748,"style":749},"186.3",[724,2347],{"x":2348,"y":734,"width":2349,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"225.5","86.7",[745,2351,758],{"x":2352,"y":748,"style":749},"268.8",[724,2354],{"x":2355,"y":734,"width":2356,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"312.2","72.3",[745,2358,766],{"x":2359,"y":748,"style":749},"348.3",[724,2361],{"x":2362,"y":734,"width":2363,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"384.4","164.9",[745,2365,2367],{"x":2366,"y":748,"style":749},"466.8","D",[724,2369],{"x":2370,"y":734,"width":2371,"height":736,"rx":727,"fill":737,"stroke":738,"style":739},"549.3","95.6",[745,2373,2375],{"x":2374,"y":748,"style":749},"597.1","E",[724,2377],{"x":2338,"y":769,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},[745,2379,775],{"x":2380,"y":774,"style":749},"131.1",[724,2382],{"x":2341,"y":769,"width":2342,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,2384,2385],{"x":2345,"y":774,"style":783},"Check at",[724,2387],{"x":2348,"y":769,"width":2349,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,2389,807],{"x":2352,"y":774,"style":783},[724,2391],{"x":2355,"y":769,"width":2356,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,2393,2394],{"x":2359,"y":774,"style":783},"Marker",[724,2396],{"x":2362,"y":769,"width":2363,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,2398,2399],{"x":2366,"y":774,"style":783},"Orders table",[724,2401],{"x":2370,"y":769,"width":2371,"height":770,"rx":727,"fill":778,"stroke":779,"style":780},[745,2403,2404],{"x":2374,"y":774,"style":783},"Result",[724,2406],{"x":2338,"y":797,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},[745,2408,801],{"x":2380,"y":800,"style":749},[724,2410],{"x":2341,"y":797,"width":2342,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2412,2413],{"x":2345,"y":800,"style":806},"07:00",[724,2415],{"x":2348,"y":797,"width":2349,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2417,2418],{"x":2352,"y":800,"style":806},"missing",[724,2420],{"x":2355,"y":797,"width":2356,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2422,2418],{"x":2359,"y":800,"style":806},[724,2424],{"x":2362,"y":797,"width":2363,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2426,2427],{"x":2366,"y":800,"style":806},"no load",[724,2429],{"x":2370,"y":797,"width":2371,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2431,2432],{"x":2374,"y":800,"style":806},"wait 1 min",[724,2434],{"x":2338,"y":820,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},[745,2436,824],{"x":2380,"y":823,"style":749},[724,2438],{"x":2341,"y":820,"width":2342,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2440,2441],{"x":2345,"y":823,"style":806},"07:01",[724,2443],{"x":2348,"y":820,"width":2349,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2445,2418],{"x":2352,"y":823,"style":806},[724,2447],{"x":2355,"y":820,"width":2356,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2449,2418],{"x":2359,"y":823,"style":806},[724,2451],{"x":2362,"y":820,"width":2363,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2453,2427],{"x":2366,"y":823,"style":806},[724,2455],{"x":2370,"y":820,"width":2371,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2457,2458],{"x":2374,"y":823,"style":806},"wait 2 min",[724,2460],{"x":2338,"y":842,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},[745,2462,846],{"x":2380,"y":845,"style":749},[724,2464],{"x":2341,"y":842,"width":2342,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2466,2467],{"x":2345,"y":845,"style":806},"07:03",[724,2469],{"x":2348,"y":842,"width":2349,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2471,2418],{"x":2352,"y":845,"style":806},[724,2473],{"x":2355,"y":842,"width":2356,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2475,2418],{"x":2359,"y":845,"style":806},[724,2477],{"x":2362,"y":842,"width":2363,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2479,2427],{"x":2366,"y":845,"style":806},[724,2481],{"x":2370,"y":842,"width":2371,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2483,2484],{"x":2374,"y":845,"style":806},"wait 4 min",[724,2486],{"x":2338,"y":2487,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"152.0",[745,2489,1811],{"x":2380,"y":2490,"style":749},"171.0",[724,2492],{"x":2341,"y":2487,"width":2342,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2494,2495],{"x":2345,"y":2490,"style":806},"07:07",[724,2497],{"x":2348,"y":2487,"width":2349,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2499,2500],{"x":2352,"y":2490,"style":806},"ready",[724,2502],{"x":2355,"y":2487,"width":2356,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2504,2500],{"x":2359,"y":2490,"style":806},[724,2506],{"x":2362,"y":2487,"width":2363,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2508,2427],{"x":2366,"y":2490,"style":806},[724,2510],{"x":2370,"y":2487,"width":2371,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2512,2513],{"x":2374,"y":2490,"style":806},"wait 8 min",[724,2515],{"x":2338,"y":2516,"width":735,"height":770,"rx":727,"fill":737,"stroke":738,"style":739},"180.0",[745,2518,1987],{"x":2380,"y":2519,"style":749},"199.0",[724,2521],{"x":2341,"y":2516,"width":2342,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2523,2524],{"x":2345,"y":2519,"style":806},"07:15",[724,2526],{"x":2348,"y":2516,"width":2349,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2528,2500],{"x":2352,"y":2519,"style":806},[724,2530],{"x":2355,"y":2516,"width":2356,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2532,2500],{"x":2359,"y":2519,"style":806},[724,2534],{"x":2362,"y":2516,"width":2363,"height":770,"rx":727,"fill":730,"stroke":738,"style":739},[745,2536,2537],{"x":2366,"y":2519,"style":806},"complete, 18,342 rows",[724,2539],{"x":2370,"y":2516,"width":2371,"height":770,"rx":727,"fill":1562,"stroke":1563,"style":780},[745,2541,2543],{"x":2374,"y":2519,"style":2542},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","build report",[20,2545,2547],{"id":2546},"choosing-the-deadline","Choosing the deadline",[10,2549,2550],{},"The deadline is a business decision more than a technical one. Work back from when the report is needed: if the morning meeting is at 09:00 and the report takes ten minutes to build and send, a deadline of 08:30 leaves a margin. Start the job early enough that a normal day finds the data ready on the first check, and use the history of when inputs actually arrived — the waiting messages are a good record if they are logged — to set the start time and spot an upstream process that is drifting later week by week.",[20,2552,2554],{"id":2553},"common-pitfalls","Common pitfalls",[2556,2557,2558,2566,2572,2578,2584],"ul",{},[2559,2560,2561,2565],"li",{},[2562,2563,2564],"strong",{},"Using the newest file."," Look for the file for the business date, so yesterday's never passes.",[2559,2567,2568,2571],{},[2562,2569,2570],{},"Treating \"exists\" as \"complete\"."," Check stability, a marker file or a load-status row.",[2559,2573,2574,2577],{},[2562,2575,2576],{},"Falling back to old data at the deadline."," Send a late-data notice instead.",[2559,2579,2580,2583],{},[2562,2581,2582],{},"Fixed one-minute polling for hours."," Grow the interval and cap it.",[2559,2585,2586,2589],{},[2562,2587,2588],{},"Testing with real waits."," Inject the clock and the sleep and simulate the morning.",[20,2591,2593],{"id":2592},"conclusion","Conclusion",[10,2595,2596],{},"Before a scheduled report builds anything, check that each input is ready for the business date — a stable export file, a producer's marker file, a complete load-status row — and poll with growing waits until a deadline. If the deadline passes, send a notice that lists each input's state instead of a report built on stale or partial data, and rerun the report once the data arrives. Inject the clock so the waiting logic can be tested in milliseconds.",[20,2598,2600],{"id":2599},"frequently-asked-questions","Frequently asked questions",[10,2602,2603,2606],{},[2562,2604,2605],{},"How do I make a Python script wait for a file before running?","\nCheck for the expected file name for the business date in a loop, sleep between checks with a growing interval, and stop at a deadline.",[10,2608,2609,2612],{},[2562,2610,2611],{},"How can I tell that a file has finished copying?","\nWait until its size and modification time have not changed for a short period, or better, have the producer write a separate marker file when it is done.",[10,2614,2615,2618],{},[2562,2616,2617],{},"What should a report do if its data is late?","\nNot run on old data; send a short notice explaining which inputs are missing, and rerun once they arrive.",[10,2620,2621,2624],{},[2562,2622,2623],{},"How do I test code that sleeps and reads the clock?","\nPass the clock and sleep functions in as parameters and replace them in tests with a fake clock that advances when asked to sleep.",[20,2626,2628],{"id":2627},"related","Related",[2556,2630,2631,2637,2644,2651],{},[2559,2632,2633,2634,2636],{},"Up one level: ",[14,2635,17],{"href":16}," — running reports on a schedule.",[2559,2638,2639,2643],{},[14,2640,2642],{"href":2641},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files\u002F","Watch a Folder and Process New Excel Files"," — reacting to files as they arrive.",[2559,2645,2646,2650],{},[14,2647,2649],{"href":2648},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fskip-a-report-when-the-input-file-has-not-changed\u002F","Skip a Report Run When the Input File Has Not Changed"," — avoiding duplicate runs.",[2559,2652,2653,2657],{},[14,2654,2656],{"href":2655},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run\u002F","Detect a Scheduled Report That Did Not Run"," — the other side of the problem.",[2659,2660,2661],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":30,"searchDepth":79,"depth":79,"links":2663},[2664,2665,2666,2667,2668,2669,2670,2671,2672,2673],{"id":22,"depth":79,"text":23},{"id":58,"depth":79,"text":59},{"id":864,"depth":79,"text":865},{"id":1155,"depth":79,"text":1156},{"id":1598,"depth":79,"text":1599},{"id":2546,"depth":79,"text":2547},{"id":2553,"depth":79,"text":2554},{"id":2592,"depth":79,"text":2593},{"id":2599,"depth":79,"text":2600},{"id":2627,"depth":79,"text":2628},"2026-10-02","Stop a scheduled Excel report from running on yesterday's data: before building, check that every input is ready — an export file dated today and finished, a marker file, a database load-status row — poll with growing intervals until a deadline, and then either run, or send a clear \"data is late\" notice instead of a wrong report. Tested with a simulated clock.","md",[2678,2680,2682,2684],{"q":2605,"a":2679},"Check for the expected file name for the business date in a loop, sleep between checks with a growing interval, and stop at a deadline.",{"q":2611,"a":2681},"Wait until its size and modification time have not changed for a short period, or better, have the producer write a separate marker file when it is done.",{"q":2617,"a":2683},"Not run on old data; send a short notice explaining which inputs are missing, and rerun once they arrive.",{"q":2623,"a":2685},"Pass the clock and sleep functions in as parameters and replace them in tests with a fake clock that advances when asked to sleep.","wait for upstream data before report",null,{"breadcrumb":2689},[2690,2692,2695],{"name":2691,"item":191},"Home",{"name":2693,"item":2694},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report",{"title":2698,"description":2699},"Wait for Upstream Data Before Running a Python Report","Readiness checks before a scheduled report: file dated today and stable, marker files, a load-status table, polling with backoff until a deadline, and a late-data notice instead of a stale report.","wait-for-upstream-data-before-running-a-report","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwait-for-upstream-data-before-running-a-report\u002Findex",[2703,2704],"scheduling","reliable-jobs","how-to","VM0JWPzCHOpLy1PAdgVgw1KI20VIPVhy3ju9yDXsgmI",[2708,2712],{"title":2709,"path":2710,"stem":2711,"children":-1},"Trigger an Excel Report from a Webhook","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Ftrigger-an-excel-report-from-a-webhook","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Ftrigger-an-excel-report-from-a-webhook\u002Findex",{"title":2713,"path":2714,"stem":2715,"children":-1},"Watch a Folder and Process New Excel Files with Python","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files\u002Findex",[2717,2719],{"path":2718,"title":2693},"\u002Fautomating-reporting-workflows",{"path":2720,"title":17},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[2723,2726,2729,2732,2735,2738],{"path":2724,"title":2725,"kind":2705},"\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":2727,"title":2728,"kind":2705},"\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":2730,"title":2731,"kind":2705},"\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":2733,"title":2734,"kind":2705},"\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":2736,"title":2737,"kind":2705},"\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":2739,"title":2656,"kind":2705},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run",1790972140223]