[{"data":1,"prerenderedAt":3508},["ShallowReactive",2],{"post:\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api\u002F":3},{"post":4,"newer":3457,"older":3467,"related":3479,"series":3506},{"id":5,"title":6,"body":7,"canonical":3439,"categories":3440,"date":3443,"description":3444,"extension":3445,"featured":3446,"hero":3447,"image":3447,"meta":3448,"navigation":434,"path":3449,"readingTime":444,"seo":3450,"series":3447,"seriesOrder":3447,"sites":3451,"source":3447,"stem":3452,"tags":3453,"updated":3447,"url":3455,"__hash__":3456},"blog\u002Fblog\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api.md","Python: Azure – Cost Anomaly Alerts from the Cost Management API",{"type":8,"value":9,"toc":3429},"minimark",[10,25,33,38,111,115,247,251,254,310,317,320,364,367,410,414,3180,3184,3234,3239,3258,3362,3369,3373,3425],[11,12,13,14,19,20,24],"p",{},"Azure Cost Management has budget alerts, but a budget is a monthly ceiling, and by the time you're close to it the anomaly that caused it has usually been running for a couple of weeks. What I actually want to know is \"did yesterday look weird compared to the last two weeks,\" which a fixed budget threshold can't tell you. This script pulls daily actual cost from the Cost Management Query API, builds a rolling baseline, and posts a webhook alert when the spend for a day old enough to have its cost data in is a statistical outlier. It is one piece of the approach in ",[15,16,18],"a",{"href":17},"\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity\u002F","Azure: Cost Governance Without Killing Developer Velocity","; the budget-threshold companion is ",[15,21,23],{"href":22},"\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts\u002F","Node.js: Azure – A Slack Bot for Resource Group Budget Alerts",".",[11,26,27,28,32],{},"Before writing your own, know what Azure already does. Cost Management runs its own anomaly detection on every subscription: it compares each day's total against a forecast trained on the last 60 days (a WaveNet-based model that accounts for patterns like Monday spikes), runs 36 hours after the end of the UTC day, and can email an ",[29,30,31],"strong",{},"anomaly alert"," when it finds something. Turn that on first (the API call is under Notes). This script earns its place when you want a different scope (a resource group, or one per team), a threshold you control, a chat channel instead of an inbox, or coverage where built-in anomaly alerts aren't offered: they exist only at subscription scope, allow five alert rules per subscription, and aren't available in Azure Government or other sovereign clouds.",[34,35,37],"h2",{"id":36},"requirements","Requirements",[39,40,41,58,64,86,93],"ul",{},[42,43,44,45,49,50,53,54,57],"li",{},"Python 3.10 or later with the ",[46,47,48],"code",{},"azure-identity"," and ",[46,51,52],{},"requests"," packages (",[46,55,56],{},"pip install azure-identity requests",").",[42,59,60,63],{},[29,61,62],{},"Cost Management Reader"," (or Cost Management Contributor) on the scope being watched. The role can be assigned at management group, subscription or resource group scope.",[42,65,66,67,70,71,74,75,74,78,81,82,85],{},"An identity ",[46,68,69],{},"DefaultAzureCredential"," can find: a managed identity when running in Azure, ",[46,72,73],{},"AZURE_CLIENT_ID"," \u002F ",[46,76,77],{},"AZURE_TENANT_ID",[46,79,80],{},"AZURE_CLIENT_SECRET"," for a service principal, or an ",[46,83,84],{},"az login"," session for testing.",[42,87,88,89,92],{},"A Slack or Microsoft Teams incoming webhook URL that accepts a JSON body with a ",[46,90,91],{},"text"," field.",[42,94,95,96,99,100,103,104,107,108,24],{},"Environment variable ",[46,97,98],{},"COST_ALERT_WEBHOOK_URL"," (not needed with ",[46,101,102],{},"--dry-run","), and ",[46,105,106],{},"AZURE_SUBSCRIPTION_ID"," unless you pass ",[46,109,110],{},"--scope",[34,112,114],{"id":113},"parameters","Parameters",[116,117,118,137],"table",{},[119,120,121],"thead",{},[122,123,124,128,131,134],"tr",{},[125,126,127],"th",{},"Name",[125,129,130],{},"Type",[125,132,133],{},"Required",[125,135,136],{},"Description",[138,139,140,163,181,199,216,233],"tbody",{},[122,141,142,147,150,153],{},[143,144,145],"td",{},[46,146,110],{},[143,148,149],{},"String",[143,151,152],{},"No",[143,154,155,156,159,160,24],{},"Cost Management scope to query, such as ",[46,157,158],{},"\u002Fsubscriptions\u002F\u003Cid>\u002FresourceGroups\u002F\u003Crg>",". Defaults to ",[46,161,162],{},"\u002Fsubscriptions\u002F$AZURE_SUBSCRIPTION_ID",[122,164,165,170,173,175],{},[143,166,167],{},[46,168,169],{},"--lookback-days",[143,171,172],{},"Int",[143,174,152],{},[143,176,177,178,24],{},"Settled days before the evaluated day used for the baseline. Default ",[46,179,180],{},"14",[122,182,183,188,191,193],{},[143,184,185],{},[46,186,187],{},"--threshold-stddev",[143,189,190],{},"Float",[143,192,152],{},[143,194,195,196,24],{},"Standard deviations from the baseline mean that count as an anomaly. Default ",[46,197,198],{},"2.5",[122,200,201,206,208,210],{},[143,202,203],{},[46,204,205],{},"--settle-days",[143,207,172],{},[143,209,152],{},[143,211,212,213,24],{},"Most recent days to skip because their data is still arriving. Default ",[46,214,215],{},"2",[122,217,218,223,225,227],{},[143,219,220],{},[46,221,222],{},"--min-delta",[143,224,190],{},[143,226,152],{},[143,228,229,230,24],{},"Ignore anomalies smaller than this absolute amount in the billing currency. Default ",[46,231,232],{},"0",[122,234,235,239,242,244],{},[143,236,237],{},[46,238,102],{},[143,240,241],{},"Switch",[143,243,152],{},[143,245,246],{},"Print the result without posting to the webhook.",[34,248,250],{"id":249},"usage","Usage",[11,252,253],{},"Run with defaults: a 14-day baseline, a 2.5 standard-deviation threshold, and the day two days ago as the evaluated day.",[255,256,261],"pre",{"className":257,"code":258,"language":259,"meta":260,"style":260},"language-bash shiki shiki-themes github-dark","export AZURE_SUBSCRIPTION_ID=\"\u003Csubscription-id>\"\nexport COST_ALERT_WEBHOOK_URL=\"\u003Chttps:\u002F\u002Fhooks.example.com\u002Fincoming-webhook>\"\npython azure_cost_anomaly_alert.py --dry-run\n","bash","",[46,262,263,283,296],{"__ignoreMap":260},[264,265,268,272,276,279],"span",{"class":266,"line":267},"line",1,[264,269,271],{"class":270},"snl16","export",[264,273,275],{"class":274},"s95oV"," AZURE_SUBSCRIPTION_ID",[264,277,278],{"class":270},"=",[264,280,282],{"class":281},"sU2Wk","\"\u003Csubscription-id>\"\n",[264,284,286,288,291,293],{"class":266,"line":285},2,[264,287,271],{"class":270},[264,289,290],{"class":274}," COST_ALERT_WEBHOOK_URL",[264,292,278],{"class":270},[264,294,295],{"class":281},"\"\u003Chttps:\u002F\u002Fhooks.example.com\u002Fincoming-webhook>\"\n",[264,297,299,303,306],{"class":266,"line":298},3,[264,300,302],{"class":301},"svObZ","python",[264,304,305],{"class":281}," azure_cost_anomaly_alert.py",[264,307,309],{"class":308},"sDLfK"," --dry-run\n",[255,311,315],{"className":312,"code":314,"language":91,"meta":260},[313],"language-text","Scope: \u002Fsubscriptions\u002F\u003Csubscription-id>\nEvaluated 2025-12-22: 612.40 USD (baseline mean 431.18, stddev 38.92, z = 4.66)\nAnomaly detected (above baseline).\nDry run: skipping webhook post.\n",[46,316,314],{"__ignoreMap":260},[11,318,319],{},"Watch one resource group with a wider baseline, a lower threshold, and a floor so a jump from 3 to 9 dollars doesn't page anyone:",[255,321,323],{"className":257,"code":322,"language":259,"meta":260,"style":260},"python azure_cost_anomaly_alert.py \\\n  --scope \"\u002Fsubscriptions\u002F\u003Csubscription-id>\u002FresourceGroups\u002Frg-prod-data\" \\\n  --lookback-days 21 --threshold-stddev 2.0 --min-delta 50\n",[46,324,325,334,344],{"__ignoreMap":260},[264,326,327,329,331],{"class":266,"line":267},[264,328,302],{"class":301},[264,330,305],{"class":281},[264,332,333],{"class":308}," \\\n",[264,335,336,339,342],{"class":266,"line":285},[264,337,338],{"class":308},"  --scope",[264,340,341],{"class":281}," \"\u002Fsubscriptions\u002F\u003Csubscription-id>\u002FresourceGroups\u002Frg-prod-data\"",[264,343,333],{"class":308},[264,345,346,349,352,355,358,361],{"class":266,"line":298},[264,347,348],{"class":308},"  --lookback-days",[264,350,351],{"class":308}," 21",[264,353,354],{"class":308}," --threshold-stddev",[264,356,357],{"class":308}," 2.0",[264,359,360],{"class":308}," --min-delta",[264,362,363],{"class":308}," 50\n",[11,365,366],{},"Schedule it once a day. Cost data refreshes about every four hours and Microsoft recommends calling the cost APIs no more than once per day, so a morning cron entry is plenty:",[255,368,370],{"className":257,"code":369,"language":259,"meta":260,"style":260},"30 9 * * * cd \u002Fopt\u002Fcost-alerts && \u002Fopt\u002Fcost-alerts\u002F.venv\u002Fbin\u002Fpython azure_cost_anomaly_alert.py >> \u002Fvar\u002Flog\u002Fcost-anomaly.log 2>&1\n",[46,371,372],{"__ignoreMap":260},[264,373,374,377,380,383,385,387,390,393,396,399,401,404,407],{"class":266,"line":267},[264,375,376],{"class":301},"30",[264,378,379],{"class":308}," 9",[264,381,382],{"class":308}," *",[264,384,382],{"class":308},[264,386,382],{"class":308},[264,388,389],{"class":281}," cd",[264,391,392],{"class":281}," \u002Fopt\u002Fcost-alerts",[264,394,395],{"class":274}," && ",[264,397,398],{"class":301},"\u002Fopt\u002Fcost-alerts\u002F.venv\u002Fbin\u002Fpython",[264,400,305],{"class":281},[264,402,403],{"class":270}," >>",[264,405,406],{"class":281}," \u002Fvar\u002Flog\u002Fcost-anomaly.log",[264,408,409],{"class":270}," 2>&1\n",[34,411,413],{"id":412},"script","Script",[255,415,418],{"className":416,"code":417,"language":302,"meta":260,"style":260},"language-python shiki shiki-themes github-dark","\"\"\"\nazure_cost_anomaly_alert.py\n\nPulls daily actual cost for an Azure scope from the Cost Management Query API,\ncompares the most recent settled day against the mean and standard deviation of\nthe preceding days, and posts a webhook alert if that day is an outlier.\n\nReads:\n    POST {scope}\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery (api-version 2025-03-01)\n\nWrites:\n    An HTTP POST with a JSON \"text\" body to a Slack- or Teams-style incoming\n    webhook when an anomaly is detected. No other side effects.\n\nEnvironment variables:\n    AZURE_SUBSCRIPTION_ID   Subscription used for the default scope.\n    COST_ALERT_WEBHOOK_URL  Incoming webhook URL to post alerts to.\n\"\"\"\n\nimport argparse\nimport os\nimport statistics\nimport sys\nimport time\nfrom datetime import date, timedelta\n\nimport requests\nfrom azure.identity import DefaultAzureCredential\n\nARM = \"https:\u002F\u002Fmanagement.azure.com\"\nAPI_VERSION = \"2025-03-01\"\nTOKEN_SCOPE = \"https:\u002F\u002Fmanagement.azure.com\u002F.default\"\nQPU_RETRY_HEADER = \"x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after\"\nMAX_ATTEMPTS = 4\n\n\ndef query_daily_costs(scope: str, start: date, end: date) -> tuple[dict, str]:\n    \"\"\"Return ({iso_date: cost}, currency) for every day from start to end inclusive.\"\"\"\n    token = DefaultAzureCredential().get_token(TOKEN_SCOPE).token\n    url = f\"{ARM}{scope}\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery?api-version={API_VERSION}\"\n    body = {\n        \"type\": \"ActualCost\",\n        \"timeframe\": \"Custom\",\n        \"timePeriod\": {\"from\": start.isoformat(), \"to\": end.isoformat()},\n        \"dataset\": {\n            \"granularity\": \"Daily\",\n            \"aggregation\": {\"totalCost\": {\"name\": \"PreTaxCost\", \"function\": \"Sum\"}},\n        },\n    }\n    headers = {\"Authorization\": f\"Bearer {token}\", \"Content-Type\": \"application\u002Fjson\"}\n\n    for attempt in range(1, MAX_ATTEMPTS + 1):\n        response = requests.post(url, json=body, headers=headers, timeout=60)\n        if response.status_code not in (429, 503) or attempt == MAX_ATTEMPTS:\n            break\n        # Throttled on query processing units (429) or service busy (503): back off as told.\n        wait = response.headers.get(QPU_RETRY_HEADER) or response.headers.get(\"Retry-After\") or \"30\"\n        print(f\"Throttled ({response.status_code}), retrying in {wait}s...\", file=sys.stderr)\n        time.sleep(float(wait))\n\n    response.raise_for_status()\n    properties = response.json()[\"properties\"]\n\n    if properties.get(\"nextLink\"):\n        print(\"Warning: response was paged; only the first page was read.\", file=sys.stderr)\n\n    columns = [col[\"name\"] for col in properties[\"columns\"]]\n    date_index = columns.index(\"UsageDate\")\n    currency_index = columns.index(\"Currency\") if \"Currency\" in columns else None\n    cost_index = next(\n        i for i, col in enumerate(properties[\"columns\"])\n        if col[\"type\"] == \"Number\" and col[\"name\"] != \"UsageDate\"\n    )\n\n    # Days with no usage are absent from the rows, so start every day at zero.\n    costs = {(start + timedelta(days=n)).isoformat(): 0.0 for n in range((end - start).days + 1)}\n    currency = \"\"\n    for row in properties[\"rows\"]:\n        raw = str(int(row[date_index]))  # UsageDate is a number such as 20251222.\n        day = date(int(raw[0:4]), int(raw[4:6]), int(raw[6:8])).isoformat()\n        costs[day] = costs.get(day, 0.0) + float(row[cost_index])\n        if currency_index is not None:\n            currency = row[currency_index]\n\n    return costs, currency\n\n\ndef evaluate(costs: dict, lookback_days: int, threshold: float, min_delta: float):\n    \"\"\"Compare the last day against the preceding lookback_days. Returns (result, is_anomaly).\"\"\"\n    days = sorted(costs)\n    if len(days) \u003C lookback_days + 1:\n        raise ValueError(f\"Need {lookback_days + 1} days of data, got {len(days)}.\")\n\n    latest = days[-1]\n    baseline = [costs[d] for d in days[-(lookback_days + 1):-1]]\n    mean = statistics.mean(baseline)\n    stddev = statistics.stdev(baseline)\n    cost = costs[latest]\n\n    result = {\"date\": latest, \"cost\": cost, \"mean\": mean, \"stddev\": stddev, \"z\": None}\n    if stddev == 0:\n        # A perfectly flat baseline: any change at all is worth a look.\n        return result, cost != mean and abs(cost - mean) >= min_delta\n\n    result[\"z\"] = (cost - mean) \u002F stddev\n    return result, abs(result[\"z\"]) >= threshold and abs(cost - mean) >= min_delta\n\n\ndef post_alert(webhook_url: str, scope: str, result: dict, currency: str) -> None:\n    direction = \"above\" if result[\"cost\"] > result[\"mean\"] else \"below\"\n    z_text = f\"{result['z']:.2f}\" if result[\"z\"] is not None else \"n\u002Fa (flat baseline)\"\n    text = (\n        f\"Azure cost anomaly on {result['date']} for `{scope}`\\n\"\n        f\"Cost: {result['cost']:,.2f} {currency} ({direction} baseline)\\n\"\n        f\"Baseline mean: {result['mean']:,.2f} {currency}, stddev {result['stddev']:,.2f}\\n\"\n        f\"Z-score: {z_text}\"\n    )\n    response = requests.post(webhook_url, json={\"text\": text}, timeout=15)\n    response.raise_for_status()\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(description=\"Detect Azure daily cost anomalies.\")\n    parser.add_argument(\"--scope\", help=\"Cost Management scope, e.g. \u002Fsubscriptions\u002F\u003Cid>\u002FresourceGroups\u002F\u003Crg>.\")\n    parser.add_argument(\"--lookback-days\", type=int, default=14)\n    parser.add_argument(\"--threshold-stddev\", type=float, default=2.5)\n    parser.add_argument(\"--settle-days\", type=int, default=2)\n    parser.add_argument(\"--min-delta\", type=float, default=0.0)\n    parser.add_argument(\"--dry-run\", action=\"store_true\")\n    args = parser.parse_args()\n\n    if args.lookback_days \u003C 2:\n        parser.error(\"--lookback-days must be at least 2.\")\n\n    scope = args.scope\n    if not scope:\n        subscription_id = os.environ.get(\"AZURE_SUBSCRIPTION_ID\")\n        if not subscription_id:\n            print(\"Set AZURE_SUBSCRIPTION_ID or pass --scope.\", file=sys.stderr)\n            return 1\n        scope = f\"\u002Fsubscriptions\u002F{subscription_id}\"\n    scope = \"\u002F\" + scope.strip(\"\u002F\")\n\n    webhook_url = os.environ.get(\"COST_ALERT_WEBHOOK_URL\")\n    if not webhook_url and not args.dry_run:\n        print(\"COST_ALERT_WEBHOOK_URL is not set.\", file=sys.stderr)\n        return 1\n\n    # Evaluate a settled day, not today: recent days are still filling in.\n    end = date.today() - timedelta(days=args.settle_days)\n    start = end - timedelta(days=args.lookback_days)\n\n    costs, currency = query_daily_costs(scope, start, end)\n    result, is_anomaly = evaluate(costs, args.lookback_days, args.threshold_stddev, args.min_delta)\n\n    z_text = f\"{result['z']:.2f}\" if result[\"z\"] is not None else \"n\u002Fa\"\n    print(f\"Scope: {scope}\")\n    print(\n        f\"Evaluated {result['date']}: {result['cost']:,.2f} {currency} \"\n        f\"(baseline mean {result['mean']:,.2f}, stddev {result['stddev']:,.2f}, z = {z_text})\"\n    )\n\n    if not is_anomaly:\n        print(\"No anomaly detected.\")\n        return 0\n\n    print(f\"Anomaly detected ({'above' if result['cost'] > result['mean'] else 'below'} baseline).\")\n    if args.dry_run:\n        print(\"Dry run: skipping webhook post.\")\n        return 0\n\n    post_alert(webhook_url, scope, result, currency)\n    print(\"Webhook alert posted.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",[46,419,420,425,430,436,442,448,454,459,465,471,476,482,488,494,499,505,511,517,522,527,536,544,552,560,568,582,587,595,608,613,625,636,647,658,669,674,679,708,714,730,762,773,788,801,822,831,844,878,884,890,935,940,974,1013,1056,1062,1069,1098,1141,1153,1158,1164,1181,1186,1200,1218,1223,1256,1272,1304,1318,1342,1375,1381,1386,1392,1446,1457,1474,1496,1550,1573,1592,1603,1608,1617,1622,1627,1657,1663,1677,1700,1742,1747,1764,1802,1813,1824,1835,1840,1884,1899,1905,1939,1944,1969,2007,2012,2017,2052,2087,2137,2148,2185,2230,2276,2293,2298,2330,2335,2340,2345,2360,2381,2402,2430,2456,2482,2508,2528,2539,2544,2559,2570,2575,2586,2596,2612,2622,2641,2650,2672,2692,2697,2712,2729,2747,2754,2759,2765,2787,2809,2814,2825,2836,2841,2885,2908,2915,2956,3002,3007,3012,3022,3034,3042,3047,3092,3099,3111,3118,3123,3129,3141,3148,3153,3158,3174],{"__ignoreMap":260},[264,421,422],{"class":266,"line":267},[264,423,424],{"class":281},"\"\"\"\n",[264,426,427],{"class":266,"line":285},[264,428,429],{"class":281},"azure_cost_anomaly_alert.py\n",[264,431,432],{"class":266,"line":298},[264,433,435],{"emptyLinePlaceholder":434},true,"\n",[264,437,439],{"class":266,"line":438},4,[264,440,441],{"class":281},"Pulls daily actual cost for an Azure scope from the Cost Management Query API,\n",[264,443,445],{"class":266,"line":444},5,[264,446,447],{"class":281},"compares the most recent settled day against the mean and standard deviation of\n",[264,449,451],{"class":266,"line":450},6,[264,452,453],{"class":281},"the preceding days, and posts a webhook alert if that day is an outlier.\n",[264,455,457],{"class":266,"line":456},7,[264,458,435],{"emptyLinePlaceholder":434},[264,460,462],{"class":266,"line":461},8,[264,463,464],{"class":281},"Reads:\n",[264,466,468],{"class":266,"line":467},9,[264,469,470],{"class":281},"    POST {scope}\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery (api-version 2025-03-01)\n",[264,472,474],{"class":266,"line":473},10,[264,475,435],{"emptyLinePlaceholder":434},[264,477,479],{"class":266,"line":478},11,[264,480,481],{"class":281},"Writes:\n",[264,483,485],{"class":266,"line":484},12,[264,486,487],{"class":281},"    An HTTP POST with a JSON \"text\" body to a Slack- or Teams-style incoming\n",[264,489,491],{"class":266,"line":490},13,[264,492,493],{"class":281},"    webhook when an anomaly is detected. No other side effects.\n",[264,495,497],{"class":266,"line":496},14,[264,498,435],{"emptyLinePlaceholder":434},[264,500,502],{"class":266,"line":501},15,[264,503,504],{"class":281},"Environment variables:\n",[264,506,508],{"class":266,"line":507},16,[264,509,510],{"class":281},"    AZURE_SUBSCRIPTION_ID   Subscription used for the default scope.\n",[264,512,514],{"class":266,"line":513},17,[264,515,516],{"class":281},"    COST_ALERT_WEBHOOK_URL  Incoming webhook URL to post alerts to.\n",[264,518,520],{"class":266,"line":519},18,[264,521,424],{"class":281},[264,523,525],{"class":266,"line":524},19,[264,526,435],{"emptyLinePlaceholder":434},[264,528,530,533],{"class":266,"line":529},20,[264,531,532],{"class":270},"import",[264,534,535],{"class":274}," argparse\n",[264,537,539,541],{"class":266,"line":538},21,[264,540,532],{"class":270},[264,542,543],{"class":274}," os\n",[264,545,547,549],{"class":266,"line":546},22,[264,548,532],{"class":270},[264,550,551],{"class":274}," statistics\n",[264,553,555,557],{"class":266,"line":554},23,[264,556,532],{"class":270},[264,558,559],{"class":274}," sys\n",[264,561,563,565],{"class":266,"line":562},24,[264,564,532],{"class":270},[264,566,567],{"class":274}," time\n",[264,569,571,574,577,579],{"class":266,"line":570},25,[264,572,573],{"class":270},"from",[264,575,576],{"class":274}," datetime ",[264,578,532],{"class":270},[264,580,581],{"class":274}," date, timedelta\n",[264,583,585],{"class":266,"line":584},26,[264,586,435],{"emptyLinePlaceholder":434},[264,588,590,592],{"class":266,"line":589},27,[264,591,532],{"class":270},[264,593,594],{"class":274}," requests\n",[264,596,598,600,603,605],{"class":266,"line":597},28,[264,599,573],{"class":270},[264,601,602],{"class":274}," azure.identity ",[264,604,532],{"class":270},[264,606,607],{"class":274}," DefaultAzureCredential\n",[264,609,611],{"class":266,"line":610},29,[264,612,435],{"emptyLinePlaceholder":434},[264,614,616,619,622],{"class":266,"line":615},30,[264,617,618],{"class":308},"ARM",[264,620,621],{"class":270}," =",[264,623,624],{"class":281}," \"https:\u002F\u002Fmanagement.azure.com\"\n",[264,626,628,631,633],{"class":266,"line":627},31,[264,629,630],{"class":308},"API_VERSION",[264,632,621],{"class":270},[264,634,635],{"class":281}," \"2025-03-01\"\n",[264,637,639,642,644],{"class":266,"line":638},32,[264,640,641],{"class":308},"TOKEN_SCOPE",[264,643,621],{"class":270},[264,645,646],{"class":281}," \"https:\u002F\u002Fmanagement.azure.com\u002F.default\"\n",[264,648,650,653,655],{"class":266,"line":649},33,[264,651,652],{"class":308},"QPU_RETRY_HEADER",[264,654,621],{"class":270},[264,656,657],{"class":281}," \"x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after\"\n",[264,659,661,664,666],{"class":266,"line":660},34,[264,662,663],{"class":308},"MAX_ATTEMPTS",[264,665,621],{"class":270},[264,667,668],{"class":308}," 4\n",[264,670,672],{"class":266,"line":671},35,[264,673,435],{"emptyLinePlaceholder":434},[264,675,677],{"class":266,"line":676},36,[264,678,435],{"emptyLinePlaceholder":434},[264,680,682,685,688,691,694,697,700,703,705],{"class":266,"line":681},37,[264,683,684],{"class":270},"def",[264,686,687],{"class":301}," query_daily_costs",[264,689,690],{"class":274},"(scope: ",[264,692,693],{"class":308},"str",[264,695,696],{"class":274},", start: date, end: date) -> tuple[",[264,698,699],{"class":308},"dict",[264,701,702],{"class":274},", ",[264,704,693],{"class":308},[264,706,707],{"class":274},"]:\n",[264,709,711],{"class":266,"line":710},38,[264,712,713],{"class":281},"    \"\"\"Return ({iso_date: cost}, currency) for every day from start to end inclusive.\"\"\"\n",[264,715,717,720,722,725,727],{"class":266,"line":716},39,[264,718,719],{"class":274},"    token ",[264,721,278],{"class":270},[264,723,724],{"class":274}," DefaultAzureCredential().get_token(",[264,726,641],{"class":308},[264,728,729],{"class":274},").token\n",[264,731,733,736,738,741,744,747,750,753,756,759],{"class":266,"line":732},40,[264,734,735],{"class":274},"    url ",[264,737,278],{"class":270},[264,739,740],{"class":270}," f",[264,742,743],{"class":281},"\"",[264,745,746],{"class":308},"{ARM}{",[264,748,749],{"class":274},"scope",[264,751,752],{"class":308},"}",[264,754,755],{"class":281},"\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery?api-version=",[264,757,758],{"class":308},"{API_VERSION}",[264,760,761],{"class":281},"\"\n",[264,763,765,768,770],{"class":266,"line":764},41,[264,766,767],{"class":274},"    body ",[264,769,278],{"class":270},[264,771,772],{"class":274}," {\n",[264,774,776,779,782,785],{"class":266,"line":775},42,[264,777,778],{"class":281},"        \"type\"",[264,780,781],{"class":274},": ",[264,783,784],{"class":281},"\"ActualCost\"",[264,786,787],{"class":274},",\n",[264,789,791,794,796,799],{"class":266,"line":790},43,[264,792,793],{"class":281},"        \"timeframe\"",[264,795,781],{"class":274},[264,797,798],{"class":281},"\"Custom\"",[264,800,787],{"class":274},[264,802,804,807,810,813,816,819],{"class":266,"line":803},44,[264,805,806],{"class":281},"        \"timePeriod\"",[264,808,809],{"class":274},": {",[264,811,812],{"class":281},"\"from\"",[264,814,815],{"class":274},": start.isoformat(), ",[264,817,818],{"class":281},"\"to\"",[264,820,821],{"class":274},": end.isoformat()},\n",[264,823,825,828],{"class":266,"line":824},45,[264,826,827],{"class":281},"        \"dataset\"",[264,829,830],{"class":274},": {\n",[264,832,834,837,839,842],{"class":266,"line":833},46,[264,835,836],{"class":281},"            \"granularity\"",[264,838,781],{"class":274},[264,840,841],{"class":281},"\"Daily\"",[264,843,787],{"class":274},[264,845,847,850,852,855,857,860,862,865,867,870,872,875],{"class":266,"line":846},47,[264,848,849],{"class":281},"            \"aggregation\"",[264,851,809],{"class":274},[264,853,854],{"class":281},"\"totalCost\"",[264,856,809],{"class":274},[264,858,859],{"class":281},"\"name\"",[264,861,781],{"class":274},[264,863,864],{"class":281},"\"PreTaxCost\"",[264,866,702],{"class":274},[264,868,869],{"class":281},"\"function\"",[264,871,781],{"class":274},[264,873,874],{"class":281},"\"Sum\"",[264,876,877],{"class":274},"}},\n",[264,879,881],{"class":266,"line":880},48,[264,882,883],{"class":274},"        },\n",[264,885,887],{"class":266,"line":886},49,[264,888,889],{"class":274},"    }\n",[264,891,893,896,898,901,904,906,909,912,915,918,920,922,924,927,929,932],{"class":266,"line":892},50,[264,894,895],{"class":274},"    headers ",[264,897,278],{"class":270},[264,899,900],{"class":274}," {",[264,902,903],{"class":281},"\"Authorization\"",[264,905,781],{"class":274},[264,907,908],{"class":270},"f",[264,910,911],{"class":281},"\"Bearer ",[264,913,914],{"class":308},"{",[264,916,917],{"class":274},"token",[264,919,752],{"class":308},[264,921,743],{"class":281},[264,923,702],{"class":274},[264,925,926],{"class":281},"\"Content-Type\"",[264,928,781],{"class":274},[264,930,931],{"class":281},"\"application\u002Fjson\"",[264,933,934],{"class":274},"}\n",[264,936,938],{"class":266,"line":937},51,[264,939,435],{"emptyLinePlaceholder":434},[264,941,943,946,949,952,955,958,961,963,965,968,971],{"class":266,"line":942},52,[264,944,945],{"class":270},"    for",[264,947,948],{"class":274}," attempt ",[264,950,951],{"class":270},"in",[264,953,954],{"class":308}," range",[264,956,957],{"class":274},"(",[264,959,960],{"class":308},"1",[264,962,702],{"class":274},[264,964,663],{"class":308},[264,966,967],{"class":270}," +",[264,969,970],{"class":308}," 1",[264,972,973],{"class":274},"):\n",[264,975,977,980,982,985,989,991,994,997,999,1002,1005,1007,1010],{"class":266,"line":976},53,[264,978,979],{"class":274},"        response ",[264,981,278],{"class":270},[264,983,984],{"class":274}," requests.post(url, ",[264,986,988],{"class":987},"s9osk","json",[264,990,278],{"class":270},[264,992,993],{"class":274},"body, ",[264,995,996],{"class":987},"headers",[264,998,278],{"class":270},[264,1000,1001],{"class":274},"headers, ",[264,1003,1004],{"class":987},"timeout",[264,1006,278],{"class":270},[264,1008,1009],{"class":308},"60",[264,1011,1012],{"class":274},")\n",[264,1014,1016,1019,1022,1025,1028,1031,1034,1036,1039,1042,1045,1047,1050,1053],{"class":266,"line":1015},54,[264,1017,1018],{"class":270},"        if",[264,1020,1021],{"class":274}," response.status_code ",[264,1023,1024],{"class":270},"not",[264,1026,1027],{"class":270}," in",[264,1029,1030],{"class":274}," (",[264,1032,1033],{"class":308},"429",[264,1035,702],{"class":274},[264,1037,1038],{"class":308},"503",[264,1040,1041],{"class":274},") ",[264,1043,1044],{"class":270},"or",[264,1046,948],{"class":274},[264,1048,1049],{"class":270},"==",[264,1051,1052],{"class":308}," MAX_ATTEMPTS",[264,1054,1055],{"class":274},":\n",[264,1057,1059],{"class":266,"line":1058},55,[264,1060,1061],{"class":270},"            break\n",[264,1063,1065],{"class":266,"line":1064},56,[264,1066,1068],{"class":1067},"sAwPA","        # Throttled on query processing units (429) or service busy (503): back off as told.\n",[264,1070,1072,1075,1077,1080,1082,1084,1086,1088,1091,1093,1095],{"class":266,"line":1071},57,[264,1073,1074],{"class":274},"        wait ",[264,1076,278],{"class":270},[264,1078,1079],{"class":274}," response.headers.get(",[264,1081,652],{"class":308},[264,1083,1041],{"class":274},[264,1085,1044],{"class":270},[264,1087,1079],{"class":274},[264,1089,1090],{"class":281},"\"Retry-After\"",[264,1092,1041],{"class":274},[264,1094,1044],{"class":270},[264,1096,1097],{"class":281}," \"30\"\n",[264,1099,1101,1104,1106,1108,1111,1113,1116,1118,1121,1123,1126,1128,1131,1133,1136,1138],{"class":266,"line":1100},58,[264,1102,1103],{"class":308},"        print",[264,1105,957],{"class":274},[264,1107,908],{"class":270},[264,1109,1110],{"class":281},"\"Throttled (",[264,1112,914],{"class":308},[264,1114,1115],{"class":274},"response.status_code",[264,1117,752],{"class":308},[264,1119,1120],{"class":281},"), retrying in ",[264,1122,914],{"class":308},[264,1124,1125],{"class":274},"wait",[264,1127,752],{"class":308},[264,1129,1130],{"class":281},"s...\"",[264,1132,702],{"class":274},[264,1134,1135],{"class":987},"file",[264,1137,278],{"class":270},[264,1139,1140],{"class":274},"sys.stderr)\n",[264,1142,1144,1147,1150],{"class":266,"line":1143},59,[264,1145,1146],{"class":274},"        time.sleep(",[264,1148,1149],{"class":308},"float",[264,1151,1152],{"class":274},"(wait))\n",[264,1154,1156],{"class":266,"line":1155},60,[264,1157,435],{"emptyLinePlaceholder":434},[264,1159,1161],{"class":266,"line":1160},61,[264,1162,1163],{"class":274},"    response.raise_for_status()\n",[264,1165,1167,1170,1172,1175,1178],{"class":266,"line":1166},62,[264,1168,1169],{"class":274},"    properties ",[264,1171,278],{"class":270},[264,1173,1174],{"class":274}," response.json()[",[264,1176,1177],{"class":281},"\"properties\"",[264,1179,1180],{"class":274},"]\n",[264,1182,1184],{"class":266,"line":1183},63,[264,1185,435],{"emptyLinePlaceholder":434},[264,1187,1189,1192,1195,1198],{"class":266,"line":1188},64,[264,1190,1191],{"class":270},"    if",[264,1193,1194],{"class":274}," properties.get(",[264,1196,1197],{"class":281},"\"nextLink\"",[264,1199,973],{"class":274},[264,1201,1203,1205,1207,1210,1212,1214,1216],{"class":266,"line":1202},65,[264,1204,1103],{"class":308},[264,1206,957],{"class":274},[264,1208,1209],{"class":281},"\"Warning: response was paged; only the first page was read.\"",[264,1211,702],{"class":274},[264,1213,1135],{"class":987},[264,1215,278],{"class":270},[264,1217,1140],{"class":274},[264,1219,1221],{"class":266,"line":1220},66,[264,1222,435],{"emptyLinePlaceholder":434},[264,1224,1226,1229,1231,1234,1236,1239,1242,1245,1247,1250,1253],{"class":266,"line":1225},67,[264,1227,1228],{"class":274},"    columns ",[264,1230,278],{"class":270},[264,1232,1233],{"class":274}," [col[",[264,1235,859],{"class":281},[264,1237,1238],{"class":274},"] ",[264,1240,1241],{"class":270},"for",[264,1243,1244],{"class":274}," col ",[264,1246,951],{"class":270},[264,1248,1249],{"class":274}," properties[",[264,1251,1252],{"class":281},"\"columns\"",[264,1254,1255],{"class":274},"]]\n",[264,1257,1259,1262,1264,1267,1270],{"class":266,"line":1258},68,[264,1260,1261],{"class":274},"    date_index ",[264,1263,278],{"class":270},[264,1265,1266],{"class":274}," columns.index(",[264,1268,1269],{"class":281},"\"UsageDate\"",[264,1271,1012],{"class":274},[264,1273,1275,1278,1280,1282,1285,1287,1290,1293,1295,1298,1301],{"class":266,"line":1274},69,[264,1276,1277],{"class":274},"    currency_index ",[264,1279,278],{"class":270},[264,1281,1266],{"class":274},[264,1283,1284],{"class":281},"\"Currency\"",[264,1286,1041],{"class":274},[264,1288,1289],{"class":270},"if",[264,1291,1292],{"class":281}," \"Currency\"",[264,1294,1027],{"class":270},[264,1296,1297],{"class":274}," columns ",[264,1299,1300],{"class":270},"else",[264,1302,1303],{"class":308}," None\n",[264,1305,1307,1310,1312,1315],{"class":266,"line":1306},70,[264,1308,1309],{"class":274},"    cost_index ",[264,1311,278],{"class":270},[264,1313,1314],{"class":308}," next",[264,1316,1317],{"class":274},"(\n",[264,1319,1321,1324,1326,1329,1331,1334,1337,1339],{"class":266,"line":1320},71,[264,1322,1323],{"class":274},"        i ",[264,1325,1241],{"class":270},[264,1327,1328],{"class":274}," i, col ",[264,1330,951],{"class":270},[264,1332,1333],{"class":308}," enumerate",[264,1335,1336],{"class":274},"(properties[",[264,1338,1252],{"class":281},[264,1340,1341],{"class":274},"])\n",[264,1343,1345,1347,1350,1353,1355,1357,1360,1363,1365,1367,1369,1372],{"class":266,"line":1344},72,[264,1346,1018],{"class":270},[264,1348,1349],{"class":274}," col[",[264,1351,1352],{"class":281},"\"type\"",[264,1354,1238],{"class":274},[264,1356,1049],{"class":270},[264,1358,1359],{"class":281}," \"Number\"",[264,1361,1362],{"class":270}," and",[264,1364,1349],{"class":274},[264,1366,859],{"class":281},[264,1368,1238],{"class":274},[264,1370,1371],{"class":270},"!=",[264,1373,1374],{"class":281}," \"UsageDate\"\n",[264,1376,1378],{"class":266,"line":1377},73,[264,1379,1380],{"class":274},"    )\n",[264,1382,1384],{"class":266,"line":1383},74,[264,1385,435],{"emptyLinePlaceholder":434},[264,1387,1389],{"class":266,"line":1388},75,[264,1390,1391],{"class":1067},"    # Days with no usage are absent from the rows, so start every day at zero.\n",[264,1393,1395,1398,1400,1403,1406,1409,1412,1414,1417,1420,1423,1426,1428,1430,1433,1436,1439,1441,1443],{"class":266,"line":1394},76,[264,1396,1397],{"class":274},"    costs ",[264,1399,278],{"class":270},[264,1401,1402],{"class":274}," {(start ",[264,1404,1405],{"class":270},"+",[264,1407,1408],{"class":274}," timedelta(",[264,1410,1411],{"class":987},"days",[264,1413,278],{"class":270},[264,1415,1416],{"class":274},"n)).isoformat(): ",[264,1418,1419],{"class":308},"0.0",[264,1421,1422],{"class":270}," for",[264,1424,1425],{"class":274}," n ",[264,1427,951],{"class":270},[264,1429,954],{"class":308},[264,1431,1432],{"class":274},"((end ",[264,1434,1435],{"class":270},"-",[264,1437,1438],{"class":274}," start).days ",[264,1440,1405],{"class":270},[264,1442,970],{"class":308},[264,1444,1445],{"class":274},")}\n",[264,1447,1449,1452,1454],{"class":266,"line":1448},77,[264,1450,1451],{"class":274},"    currency ",[264,1453,278],{"class":270},[264,1455,1456],{"class":281}," \"\"\n",[264,1458,1460,1462,1465,1467,1469,1472],{"class":266,"line":1459},78,[264,1461,945],{"class":270},[264,1463,1464],{"class":274}," row ",[264,1466,951],{"class":270},[264,1468,1249],{"class":274},[264,1470,1471],{"class":281},"\"rows\"",[264,1473,707],{"class":274},[264,1475,1477,1480,1482,1485,1487,1490,1493],{"class":266,"line":1476},79,[264,1478,1479],{"class":274},"        raw ",[264,1481,278],{"class":270},[264,1483,1484],{"class":308}," str",[264,1486,957],{"class":274},[264,1488,1489],{"class":308},"int",[264,1491,1492],{"class":274},"(row[date_index]))  ",[264,1494,1495],{"class":1067},"# UsageDate is a number such as 20251222.\n",[264,1497,1499,1502,1504,1507,1509,1512,1514,1517,1520,1523,1525,1527,1529,1531,1534,1536,1538,1540,1542,1544,1547],{"class":266,"line":1498},80,[264,1500,1501],{"class":274},"        day ",[264,1503,278],{"class":270},[264,1505,1506],{"class":274}," date(",[264,1508,1489],{"class":308},[264,1510,1511],{"class":274},"(raw[",[264,1513,232],{"class":308},[264,1515,1516],{"class":274},":",[264,1518,1519],{"class":308},"4",[264,1521,1522],{"class":274},"]), ",[264,1524,1489],{"class":308},[264,1526,1511],{"class":274},[264,1528,1519],{"class":308},[264,1530,1516],{"class":274},[264,1532,1533],{"class":308},"6",[264,1535,1522],{"class":274},[264,1537,1489],{"class":308},[264,1539,1511],{"class":274},[264,1541,1533],{"class":308},[264,1543,1516],{"class":274},[264,1545,1546],{"class":308},"8",[264,1548,1549],{"class":274},"])).isoformat()\n",[264,1551,1553,1556,1558,1561,1563,1565,1567,1570],{"class":266,"line":1552},81,[264,1554,1555],{"class":274},"        costs[day] ",[264,1557,278],{"class":270},[264,1559,1560],{"class":274}," costs.get(day, ",[264,1562,1419],{"class":308},[264,1564,1041],{"class":274},[264,1566,1405],{"class":270},[264,1568,1569],{"class":308}," float",[264,1571,1572],{"class":274},"(row[cost_index])\n",[264,1574,1576,1578,1581,1584,1587,1590],{"class":266,"line":1575},82,[264,1577,1018],{"class":270},[264,1579,1580],{"class":274}," currency_index ",[264,1582,1583],{"class":270},"is",[264,1585,1586],{"class":270}," not",[264,1588,1589],{"class":308}," None",[264,1591,1055],{"class":274},[264,1593,1595,1598,1600],{"class":266,"line":1594},83,[264,1596,1597],{"class":274},"            currency ",[264,1599,278],{"class":270},[264,1601,1602],{"class":274}," row[currency_index]\n",[264,1604,1606],{"class":266,"line":1605},84,[264,1607,435],{"emptyLinePlaceholder":434},[264,1609,1611,1614],{"class":266,"line":1610},85,[264,1612,1613],{"class":270},"    return",[264,1615,1616],{"class":274}," costs, currency\n",[264,1618,1620],{"class":266,"line":1619},86,[264,1621,435],{"emptyLinePlaceholder":434},[264,1623,1625],{"class":266,"line":1624},87,[264,1626,435],{"emptyLinePlaceholder":434},[264,1628,1630,1632,1635,1638,1640,1643,1645,1648,1650,1653,1655],{"class":266,"line":1629},88,[264,1631,684],{"class":270},[264,1633,1634],{"class":301}," evaluate",[264,1636,1637],{"class":274},"(costs: ",[264,1639,699],{"class":308},[264,1641,1642],{"class":274},", lookback_days: ",[264,1644,1489],{"class":308},[264,1646,1647],{"class":274},", threshold: ",[264,1649,1149],{"class":308},[264,1651,1652],{"class":274},", min_delta: ",[264,1654,1149],{"class":308},[264,1656,973],{"class":274},[264,1658,1660],{"class":266,"line":1659},89,[264,1661,1662],{"class":281},"    \"\"\"Compare the last day against the preceding lookback_days. Returns (result, is_anomaly).\"\"\"\n",[264,1664,1666,1669,1671,1674],{"class":266,"line":1665},90,[264,1667,1668],{"class":274},"    days ",[264,1670,278],{"class":270},[264,1672,1673],{"class":308}," sorted",[264,1675,1676],{"class":274},"(costs)\n",[264,1678,1680,1682,1685,1688,1691,1694,1696,1698],{"class":266,"line":1679},91,[264,1681,1191],{"class":270},[264,1683,1684],{"class":308}," len",[264,1686,1687],{"class":274},"(days) ",[264,1689,1690],{"class":270},"\u003C",[264,1692,1693],{"class":274}," lookback_days ",[264,1695,1405],{"class":270},[264,1697,970],{"class":308},[264,1699,1055],{"class":274},[264,1701,1703,1706,1709,1711,1713,1716,1718,1721,1723,1726,1729,1732,1735,1737,1740],{"class":266,"line":1702},92,[264,1704,1705],{"class":270},"        raise",[264,1707,1708],{"class":308}," ValueError",[264,1710,957],{"class":274},[264,1712,908],{"class":270},[264,1714,1715],{"class":281},"\"Need ",[264,1717,914],{"class":308},[264,1719,1720],{"class":274},"lookback_days ",[264,1722,1405],{"class":270},[264,1724,1725],{"class":308}," 1}",[264,1727,1728],{"class":281}," days of data, got ",[264,1730,1731],{"class":308},"{len",[264,1733,1734],{"class":274},"(days)",[264,1736,752],{"class":308},[264,1738,1739],{"class":281},".\"",[264,1741,1012],{"class":274},[264,1743,1745],{"class":266,"line":1744},93,[264,1746,435],{"emptyLinePlaceholder":434},[264,1748,1750,1753,1755,1758,1760,1762],{"class":266,"line":1749},94,[264,1751,1752],{"class":274},"    latest ",[264,1754,278],{"class":270},[264,1756,1757],{"class":274}," days[",[264,1759,1435],{"class":270},[264,1761,960],{"class":308},[264,1763,1180],{"class":274},[264,1765,1767,1770,1772,1775,1777,1780,1782,1784,1786,1789,1791,1793,1796,1798,1800],{"class":266,"line":1766},95,[264,1768,1769],{"class":274},"    baseline ",[264,1771,278],{"class":270},[264,1773,1774],{"class":274}," [costs[d] ",[264,1776,1241],{"class":270},[264,1778,1779],{"class":274}," d ",[264,1781,951],{"class":270},[264,1783,1757],{"class":274},[264,1785,1435],{"class":270},[264,1787,1788],{"class":274},"(lookback_days ",[264,1790,1405],{"class":270},[264,1792,970],{"class":308},[264,1794,1795],{"class":274},"):",[264,1797,1435],{"class":270},[264,1799,960],{"class":308},[264,1801,1255],{"class":274},[264,1803,1805,1808,1810],{"class":266,"line":1804},96,[264,1806,1807],{"class":274},"    mean ",[264,1809,278],{"class":270},[264,1811,1812],{"class":274}," statistics.mean(baseline)\n",[264,1814,1816,1819,1821],{"class":266,"line":1815},97,[264,1817,1818],{"class":274},"    stddev ",[264,1820,278],{"class":270},[264,1822,1823],{"class":274}," statistics.stdev(baseline)\n",[264,1825,1827,1830,1832],{"class":266,"line":1826},98,[264,1828,1829],{"class":274},"    cost ",[264,1831,278],{"class":270},[264,1833,1834],{"class":274}," costs[latest]\n",[264,1836,1838],{"class":266,"line":1837},99,[264,1839,435],{"emptyLinePlaceholder":434},[264,1841,1843,1846,1848,1850,1853,1856,1859,1862,1865,1868,1871,1874,1877,1879,1882],{"class":266,"line":1842},100,[264,1844,1845],{"class":274},"    result ",[264,1847,278],{"class":270},[264,1849,900],{"class":274},[264,1851,1852],{"class":281},"\"date\"",[264,1854,1855],{"class":274},": latest, ",[264,1857,1858],{"class":281},"\"cost\"",[264,1860,1861],{"class":274},": cost, ",[264,1863,1864],{"class":281},"\"mean\"",[264,1866,1867],{"class":274},": mean, ",[264,1869,1870],{"class":281},"\"stddev\"",[264,1872,1873],{"class":274},": stddev, ",[264,1875,1876],{"class":281},"\"z\"",[264,1878,781],{"class":274},[264,1880,1881],{"class":308},"None",[264,1883,934],{"class":274},[264,1885,1887,1889,1892,1894,1897],{"class":266,"line":1886},101,[264,1888,1191],{"class":270},[264,1890,1891],{"class":274}," stddev ",[264,1893,1049],{"class":270},[264,1895,1896],{"class":308}," 0",[264,1898,1055],{"class":274},[264,1900,1902],{"class":266,"line":1901},102,[264,1903,1904],{"class":1067},"        # A perfectly flat baseline: any change at all is worth a look.\n",[264,1906,1908,1911,1914,1916,1919,1922,1925,1928,1930,1933,1936],{"class":266,"line":1907},103,[264,1909,1910],{"class":270},"        return",[264,1912,1913],{"class":274}," result, cost ",[264,1915,1371],{"class":270},[264,1917,1918],{"class":274}," mean ",[264,1920,1921],{"class":270},"and",[264,1923,1924],{"class":308}," abs",[264,1926,1927],{"class":274},"(cost ",[264,1929,1435],{"class":270},[264,1931,1932],{"class":274}," mean) ",[264,1934,1935],{"class":270},">=",[264,1937,1938],{"class":274}," min_delta\n",[264,1940,1942],{"class":266,"line":1941},104,[264,1943,435],{"emptyLinePlaceholder":434},[264,1945,1947,1950,1952,1954,1956,1959,1961,1963,1966],{"class":266,"line":1946},105,[264,1948,1949],{"class":274},"    result[",[264,1951,1876],{"class":281},[264,1953,1238],{"class":274},[264,1955,278],{"class":270},[264,1957,1958],{"class":274}," (cost ",[264,1960,1435],{"class":270},[264,1962,1932],{"class":274},[264,1964,1965],{"class":270},"\u002F",[264,1967,1968],{"class":274}," stddev\n",[264,1970,1972,1974,1977,1980,1983,1985,1988,1990,1993,1995,1997,1999,2001,2003,2005],{"class":266,"line":1971},106,[264,1973,1613],{"class":270},[264,1975,1976],{"class":274}," result, ",[264,1978,1979],{"class":308},"abs",[264,1981,1982],{"class":274},"(result[",[264,1984,1876],{"class":281},[264,1986,1987],{"class":274},"]) ",[264,1989,1935],{"class":270},[264,1991,1992],{"class":274}," threshold ",[264,1994,1921],{"class":270},[264,1996,1924],{"class":308},[264,1998,1927],{"class":274},[264,2000,1435],{"class":270},[264,2002,1932],{"class":274},[264,2004,1935],{"class":270},[264,2006,1938],{"class":274},[264,2008,2010],{"class":266,"line":2009},107,[264,2011,435],{"emptyLinePlaceholder":434},[264,2013,2015],{"class":266,"line":2014},108,[264,2016,435],{"emptyLinePlaceholder":434},[264,2018,2020,2022,2025,2028,2030,2033,2035,2038,2040,2043,2045,2048,2050],{"class":266,"line":2019},109,[264,2021,684],{"class":270},[264,2023,2024],{"class":301}," post_alert",[264,2026,2027],{"class":274},"(webhook_url: ",[264,2029,693],{"class":308},[264,2031,2032],{"class":274},", scope: ",[264,2034,693],{"class":308},[264,2036,2037],{"class":274},", result: ",[264,2039,699],{"class":308},[264,2041,2042],{"class":274},", currency: ",[264,2044,693],{"class":308},[264,2046,2047],{"class":274},") -> ",[264,2049,1881],{"class":308},[264,2051,1055],{"class":274},[264,2053,2055,2058,2060,2063,2066,2069,2071,2073,2076,2078,2080,2082,2084],{"class":266,"line":2054},110,[264,2056,2057],{"class":274},"    direction ",[264,2059,278],{"class":270},[264,2061,2062],{"class":281}," \"above\"",[264,2064,2065],{"class":270}," if",[264,2067,2068],{"class":274}," result[",[264,2070,1858],{"class":281},[264,2072,1238],{"class":274},[264,2074,2075],{"class":270},">",[264,2077,2068],{"class":274},[264,2079,1864],{"class":281},[264,2081,1238],{"class":274},[264,2083,1300],{"class":270},[264,2085,2086],{"class":281}," \"below\"\n",[264,2088,2090,2093,2095,2097,2099,2101,2104,2107,2110,2113,2115,2117,2119,2121,2123,2125,2127,2129,2131,2134],{"class":266,"line":2089},111,[264,2091,2092],{"class":274},"    z_text ",[264,2094,278],{"class":270},[264,2096,740],{"class":270},[264,2098,743],{"class":281},[264,2100,914],{"class":308},[264,2102,2103],{"class":274},"result[",[264,2105,2106],{"class":281},"'z'",[264,2108,2109],{"class":274},"]",[264,2111,2112],{"class":270},":.2f",[264,2114,752],{"class":308},[264,2116,743],{"class":281},[264,2118,2065],{"class":270},[264,2120,2068],{"class":274},[264,2122,1876],{"class":281},[264,2124,1238],{"class":274},[264,2126,1583],{"class":270},[264,2128,1586],{"class":270},[264,2130,1589],{"class":308},[264,2132,2133],{"class":270}," else",[264,2135,2136],{"class":281}," \"n\u002Fa (flat baseline)\"\n",[264,2138,2140,2143,2145],{"class":266,"line":2139},112,[264,2141,2142],{"class":274},"    text ",[264,2144,278],{"class":270},[264,2146,2147],{"class":274}," (\n",[264,2149,2151,2154,2157,2159,2161,2164,2166,2168,2171,2173,2175,2177,2180,2183],{"class":266,"line":2150},113,[264,2152,2153],{"class":270},"        f",[264,2155,2156],{"class":281},"\"Azure cost anomaly on ",[264,2158,914],{"class":308},[264,2160,2103],{"class":274},[264,2162,2163],{"class":281},"'date'",[264,2165,2109],{"class":274},[264,2167,752],{"class":308},[264,2169,2170],{"class":281}," for `",[264,2172,914],{"class":308},[264,2174,749],{"class":274},[264,2176,752],{"class":308},[264,2178,2179],{"class":281},"`",[264,2181,2182],{"class":308},"\\n",[264,2184,761],{"class":281},[264,2186,2188,2190,2193,2195,2197,2200,2202,2205,2207,2209,2212,2214,2216,2218,2221,2223,2226,2228],{"class":266,"line":2187},114,[264,2189,2153],{"class":270},[264,2191,2192],{"class":281},"\"Cost: ",[264,2194,914],{"class":308},[264,2196,2103],{"class":274},[264,2198,2199],{"class":281},"'cost'",[264,2201,2109],{"class":274},[264,2203,2204],{"class":270},":,.2f",[264,2206,752],{"class":308},[264,2208,900],{"class":308},[264,2210,2211],{"class":274},"currency",[264,2213,752],{"class":308},[264,2215,1030],{"class":281},[264,2217,914],{"class":308},[264,2219,2220],{"class":274},"direction",[264,2222,752],{"class":308},[264,2224,2225],{"class":281}," baseline)",[264,2227,2182],{"class":308},[264,2229,761],{"class":281},[264,2231,2233,2235,2238,2240,2242,2245,2247,2249,2251,2253,2255,2257,2260,2262,2264,2267,2269,2271,2274],{"class":266,"line":2232},115,[264,2234,2153],{"class":270},[264,2236,2237],{"class":281},"\"Baseline mean: ",[264,2239,914],{"class":308},[264,2241,2103],{"class":274},[264,2243,2244],{"class":281},"'mean'",[264,2246,2109],{"class":274},[264,2248,2204],{"class":270},[264,2250,752],{"class":308},[264,2252,900],{"class":308},[264,2254,2211],{"class":274},[264,2256,752],{"class":308},[264,2258,2259],{"class":281},", stddev ",[264,2261,914],{"class":308},[264,2263,2103],{"class":274},[264,2265,2266],{"class":281},"'stddev'",[264,2268,2109],{"class":274},[264,2270,2204],{"class":270},[264,2272,2273],{"class":308},"}\\n",[264,2275,761],{"class":281},[264,2277,2279,2281,2284,2286,2289,2291],{"class":266,"line":2278},116,[264,2280,2153],{"class":270},[264,2282,2283],{"class":281},"\"Z-score: ",[264,2285,914],{"class":308},[264,2287,2288],{"class":274},"z_text",[264,2290,752],{"class":308},[264,2292,761],{"class":281},[264,2294,2296],{"class":266,"line":2295},117,[264,2297,1380],{"class":274},[264,2299,2301,2304,2306,2309,2311,2313,2315,2318,2321,2323,2325,2328],{"class":266,"line":2300},118,[264,2302,2303],{"class":274},"    response ",[264,2305,278],{"class":270},[264,2307,2308],{"class":274}," requests.post(webhook_url, ",[264,2310,988],{"class":987},[264,2312,278],{"class":270},[264,2314,914],{"class":274},[264,2316,2317],{"class":281},"\"text\"",[264,2319,2320],{"class":274},": text}, ",[264,2322,1004],{"class":987},[264,2324,278],{"class":270},[264,2326,2327],{"class":308},"15",[264,2329,1012],{"class":274},[264,2331,2333],{"class":266,"line":2332},119,[264,2334,1163],{"class":274},[264,2336,2338],{"class":266,"line":2337},120,[264,2339,435],{"emptyLinePlaceholder":434},[264,2341,2343],{"class":266,"line":2342},121,[264,2344,435],{"emptyLinePlaceholder":434},[264,2346,2348,2350,2353,2356,2358],{"class":266,"line":2347},122,[264,2349,684],{"class":270},[264,2351,2352],{"class":301}," main",[264,2354,2355],{"class":274},"() -> ",[264,2357,1489],{"class":308},[264,2359,1055],{"class":274},[264,2361,2363,2366,2368,2371,2374,2376,2379],{"class":266,"line":2362},123,[264,2364,2365],{"class":274},"    parser ",[264,2367,278],{"class":270},[264,2369,2370],{"class":274}," argparse.ArgumentParser(",[264,2372,2373],{"class":987},"description",[264,2375,278],{"class":270},[264,2377,2378],{"class":281},"\"Detect Azure daily cost anomalies.\"",[264,2380,1012],{"class":274},[264,2382,2384,2387,2390,2392,2395,2397,2400],{"class":266,"line":2383},124,[264,2385,2386],{"class":274},"    parser.add_argument(",[264,2388,2389],{"class":281},"\"--scope\"",[264,2391,702],{"class":274},[264,2393,2394],{"class":987},"help",[264,2396,278],{"class":270},[264,2398,2399],{"class":281},"\"Cost Management scope, e.g. \u002Fsubscriptions\u002F\u003Cid>\u002FresourceGroups\u002F\u003Crg>.\"",[264,2401,1012],{"class":274},[264,2403,2405,2407,2410,2412,2415,2417,2419,2421,2424,2426,2428],{"class":266,"line":2404},125,[264,2406,2386],{"class":274},[264,2408,2409],{"class":281},"\"--lookback-days\"",[264,2411,702],{"class":274},[264,2413,2414],{"class":987},"type",[264,2416,278],{"class":270},[264,2418,1489],{"class":308},[264,2420,702],{"class":274},[264,2422,2423],{"class":987},"default",[264,2425,278],{"class":270},[264,2427,180],{"class":308},[264,2429,1012],{"class":274},[264,2431,2433,2435,2438,2440,2442,2444,2446,2448,2450,2452,2454],{"class":266,"line":2432},126,[264,2434,2386],{"class":274},[264,2436,2437],{"class":281},"\"--threshold-stddev\"",[264,2439,702],{"class":274},[264,2441,2414],{"class":987},[264,2443,278],{"class":270},[264,2445,1149],{"class":308},[264,2447,702],{"class":274},[264,2449,2423],{"class":987},[264,2451,278],{"class":270},[264,2453,198],{"class":308},[264,2455,1012],{"class":274},[264,2457,2459,2461,2464,2466,2468,2470,2472,2474,2476,2478,2480],{"class":266,"line":2458},127,[264,2460,2386],{"class":274},[264,2462,2463],{"class":281},"\"--settle-days\"",[264,2465,702],{"class":274},[264,2467,2414],{"class":987},[264,2469,278],{"class":270},[264,2471,1489],{"class":308},[264,2473,702],{"class":274},[264,2475,2423],{"class":987},[264,2477,278],{"class":270},[264,2479,215],{"class":308},[264,2481,1012],{"class":274},[264,2483,2485,2487,2490,2492,2494,2496,2498,2500,2502,2504,2506],{"class":266,"line":2484},128,[264,2486,2386],{"class":274},[264,2488,2489],{"class":281},"\"--min-delta\"",[264,2491,702],{"class":274},[264,2493,2414],{"class":987},[264,2495,278],{"class":270},[264,2497,1149],{"class":308},[264,2499,702],{"class":274},[264,2501,2423],{"class":987},[264,2503,278],{"class":270},[264,2505,1419],{"class":308},[264,2507,1012],{"class":274},[264,2509,2511,2513,2516,2518,2521,2523,2526],{"class":266,"line":2510},129,[264,2512,2386],{"class":274},[264,2514,2515],{"class":281},"\"--dry-run\"",[264,2517,702],{"class":274},[264,2519,2520],{"class":987},"action",[264,2522,278],{"class":270},[264,2524,2525],{"class":281},"\"store_true\"",[264,2527,1012],{"class":274},[264,2529,2531,2534,2536],{"class":266,"line":2530},130,[264,2532,2533],{"class":274},"    args ",[264,2535,278],{"class":270},[264,2537,2538],{"class":274}," parser.parse_args()\n",[264,2540,2542],{"class":266,"line":2541},131,[264,2543,435],{"emptyLinePlaceholder":434},[264,2545,2547,2549,2552,2554,2557],{"class":266,"line":2546},132,[264,2548,1191],{"class":270},[264,2550,2551],{"class":274}," args.lookback_days ",[264,2553,1690],{"class":270},[264,2555,2556],{"class":308}," 2",[264,2558,1055],{"class":274},[264,2560,2562,2565,2568],{"class":266,"line":2561},133,[264,2563,2564],{"class":274},"        parser.error(",[264,2566,2567],{"class":281},"\"--lookback-days must be at least 2.\"",[264,2569,1012],{"class":274},[264,2571,2573],{"class":266,"line":2572},134,[264,2574,435],{"emptyLinePlaceholder":434},[264,2576,2578,2581,2583],{"class":266,"line":2577},135,[264,2579,2580],{"class":274},"    scope ",[264,2582,278],{"class":270},[264,2584,2585],{"class":274}," args.scope\n",[264,2587,2589,2591,2593],{"class":266,"line":2588},136,[264,2590,1191],{"class":270},[264,2592,1586],{"class":270},[264,2594,2595],{"class":274}," scope:\n",[264,2597,2599,2602,2604,2607,2610],{"class":266,"line":2598},137,[264,2600,2601],{"class":274},"        subscription_id ",[264,2603,278],{"class":270},[264,2605,2606],{"class":274}," os.environ.get(",[264,2608,2609],{"class":281},"\"AZURE_SUBSCRIPTION_ID\"",[264,2611,1012],{"class":274},[264,2613,2615,2617,2619],{"class":266,"line":2614},138,[264,2616,1018],{"class":270},[264,2618,1586],{"class":270},[264,2620,2621],{"class":274}," subscription_id:\n",[264,2623,2625,2628,2630,2633,2635,2637,2639],{"class":266,"line":2624},139,[264,2626,2627],{"class":308},"            print",[264,2629,957],{"class":274},[264,2631,2632],{"class":281},"\"Set AZURE_SUBSCRIPTION_ID or pass --scope.\"",[264,2634,702],{"class":274},[264,2636,1135],{"class":987},[264,2638,278],{"class":270},[264,2640,1140],{"class":274},[264,2642,2644,2647],{"class":266,"line":2643},140,[264,2645,2646],{"class":270},"            return",[264,2648,2649],{"class":308}," 1\n",[264,2651,2653,2656,2658,2660,2663,2665,2668,2670],{"class":266,"line":2652},141,[264,2654,2655],{"class":274},"        scope ",[264,2657,278],{"class":270},[264,2659,740],{"class":270},[264,2661,2662],{"class":281},"\"\u002Fsubscriptions\u002F",[264,2664,914],{"class":308},[264,2666,2667],{"class":274},"subscription_id",[264,2669,752],{"class":308},[264,2671,761],{"class":281},[264,2673,2675,2677,2679,2682,2684,2687,2690],{"class":266,"line":2674},142,[264,2676,2580],{"class":274},[264,2678,278],{"class":270},[264,2680,2681],{"class":281}," \"\u002F\"",[264,2683,967],{"class":270},[264,2685,2686],{"class":274}," scope.strip(",[264,2688,2689],{"class":281},"\"\u002F\"",[264,2691,1012],{"class":274},[264,2693,2695],{"class":266,"line":2694},143,[264,2696,435],{"emptyLinePlaceholder":434},[264,2698,2700,2703,2705,2707,2710],{"class":266,"line":2699},144,[264,2701,2702],{"class":274},"    webhook_url ",[264,2704,278],{"class":270},[264,2706,2606],{"class":274},[264,2708,2709],{"class":281},"\"COST_ALERT_WEBHOOK_URL\"",[264,2711,1012],{"class":274},[264,2713,2715,2717,2719,2722,2724,2726],{"class":266,"line":2714},145,[264,2716,1191],{"class":270},[264,2718,1586],{"class":270},[264,2720,2721],{"class":274}," webhook_url ",[264,2723,1921],{"class":270},[264,2725,1586],{"class":270},[264,2727,2728],{"class":274}," args.dry_run:\n",[264,2730,2732,2734,2736,2739,2741,2743,2745],{"class":266,"line":2731},146,[264,2733,1103],{"class":308},[264,2735,957],{"class":274},[264,2737,2738],{"class":281},"\"COST_ALERT_WEBHOOK_URL is not set.\"",[264,2740,702],{"class":274},[264,2742,1135],{"class":987},[264,2744,278],{"class":270},[264,2746,1140],{"class":274},[264,2748,2750,2752],{"class":266,"line":2749},147,[264,2751,1910],{"class":270},[264,2753,2649],{"class":308},[264,2755,2757],{"class":266,"line":2756},148,[264,2758,435],{"emptyLinePlaceholder":434},[264,2760,2762],{"class":266,"line":2761},149,[264,2763,2764],{"class":1067},"    # Evaluate a settled day, not today: recent days are still filling in.\n",[264,2766,2768,2771,2773,2776,2778,2780,2782,2784],{"class":266,"line":2767},150,[264,2769,2770],{"class":274},"    end ",[264,2772,278],{"class":270},[264,2774,2775],{"class":274}," date.today() ",[264,2777,1435],{"class":270},[264,2779,1408],{"class":274},[264,2781,1411],{"class":987},[264,2783,278],{"class":270},[264,2785,2786],{"class":274},"args.settle_days)\n",[264,2788,2790,2793,2795,2798,2800,2802,2804,2806],{"class":266,"line":2789},151,[264,2791,2792],{"class":274},"    start ",[264,2794,278],{"class":270},[264,2796,2797],{"class":274}," end ",[264,2799,1435],{"class":270},[264,2801,1408],{"class":274},[264,2803,1411],{"class":987},[264,2805,278],{"class":270},[264,2807,2808],{"class":274},"args.lookback_days)\n",[264,2810,2812],{"class":266,"line":2811},152,[264,2813,435],{"emptyLinePlaceholder":434},[264,2815,2817,2820,2822],{"class":266,"line":2816},153,[264,2818,2819],{"class":274},"    costs, currency ",[264,2821,278],{"class":270},[264,2823,2824],{"class":274}," query_daily_costs(scope, start, end)\n",[264,2826,2828,2831,2833],{"class":266,"line":2827},154,[264,2829,2830],{"class":274},"    result, is_anomaly ",[264,2832,278],{"class":270},[264,2834,2835],{"class":274}," evaluate(costs, args.lookback_days, args.threshold_stddev, args.min_delta)\n",[264,2837,2839],{"class":266,"line":2838},155,[264,2840,435],{"emptyLinePlaceholder":434},[264,2842,2844,2846,2848,2850,2852,2854,2856,2858,2860,2862,2864,2866,2868,2870,2872,2874,2876,2878,2880,2882],{"class":266,"line":2843},156,[264,2845,2092],{"class":274},[264,2847,278],{"class":270},[264,2849,740],{"class":270},[264,2851,743],{"class":281},[264,2853,914],{"class":308},[264,2855,2103],{"class":274},[264,2857,2106],{"class":281},[264,2859,2109],{"class":274},[264,2861,2112],{"class":270},[264,2863,752],{"class":308},[264,2865,743],{"class":281},[264,2867,2065],{"class":270},[264,2869,2068],{"class":274},[264,2871,1876],{"class":281},[264,2873,1238],{"class":274},[264,2875,1583],{"class":270},[264,2877,1586],{"class":270},[264,2879,1589],{"class":308},[264,2881,2133],{"class":270},[264,2883,2884],{"class":281}," \"n\u002Fa\"\n",[264,2886,2888,2891,2893,2895,2898,2900,2902,2904,2906],{"class":266,"line":2887},157,[264,2889,2890],{"class":308},"    print",[264,2892,957],{"class":274},[264,2894,908],{"class":270},[264,2896,2897],{"class":281},"\"Scope: ",[264,2899,914],{"class":308},[264,2901,749],{"class":274},[264,2903,752],{"class":308},[264,2905,743],{"class":281},[264,2907,1012],{"class":274},[264,2909,2911,2913],{"class":266,"line":2910},158,[264,2912,2890],{"class":308},[264,2914,1317],{"class":274},[264,2916,2918,2920,2923,2925,2927,2929,2931,2933,2935,2937,2939,2941,2943,2945,2947,2949,2951,2953],{"class":266,"line":2917},159,[264,2919,2153],{"class":270},[264,2921,2922],{"class":281},"\"Evaluated ",[264,2924,914],{"class":308},[264,2926,2103],{"class":274},[264,2928,2163],{"class":281},[264,2930,2109],{"class":274},[264,2932,752],{"class":308},[264,2934,781],{"class":281},[264,2936,914],{"class":308},[264,2938,2103],{"class":274},[264,2940,2199],{"class":281},[264,2942,2109],{"class":274},[264,2944,2204],{"class":270},[264,2946,752],{"class":308},[264,2948,900],{"class":308},[264,2950,2211],{"class":274},[264,2952,752],{"class":308},[264,2954,2955],{"class":281}," \"\n",[264,2957,2959,2961,2964,2966,2968,2970,2972,2974,2976,2978,2980,2982,2984,2986,2988,2990,2993,2995,2997,2999],{"class":266,"line":2958},160,[264,2960,2153],{"class":270},[264,2962,2963],{"class":281},"\"(baseline mean ",[264,2965,914],{"class":308},[264,2967,2103],{"class":274},[264,2969,2244],{"class":281},[264,2971,2109],{"class":274},[264,2973,2204],{"class":270},[264,2975,752],{"class":308},[264,2977,2259],{"class":281},[264,2979,914],{"class":308},[264,2981,2103],{"class":274},[264,2983,2266],{"class":281},[264,2985,2109],{"class":274},[264,2987,2204],{"class":270},[264,2989,752],{"class":308},[264,2991,2992],{"class":281},", z = ",[264,2994,914],{"class":308},[264,2996,2288],{"class":274},[264,2998,752],{"class":308},[264,3000,3001],{"class":281},")\"\n",[264,3003,3005],{"class":266,"line":3004},161,[264,3006,1380],{"class":274},[264,3008,3010],{"class":266,"line":3009},162,[264,3011,435],{"emptyLinePlaceholder":434},[264,3013,3015,3017,3019],{"class":266,"line":3014},163,[264,3016,1191],{"class":270},[264,3018,1586],{"class":270},[264,3020,3021],{"class":274}," is_anomaly:\n",[264,3023,3025,3027,3029,3032],{"class":266,"line":3024},164,[264,3026,1103],{"class":308},[264,3028,957],{"class":274},[264,3030,3031],{"class":281},"\"No anomaly detected.\"",[264,3033,1012],{"class":274},[264,3035,3037,3039],{"class":266,"line":3036},165,[264,3038,1910],{"class":270},[264,3040,3041],{"class":308}," 0\n",[264,3043,3045],{"class":266,"line":3044},166,[264,3046,435],{"emptyLinePlaceholder":434},[264,3048,3050,3052,3054,3056,3059,3061,3064,3066,3068,3070,3072,3074,3076,3078,3080,3082,3085,3087,3090],{"class":266,"line":3049},167,[264,3051,2890],{"class":308},[264,3053,957],{"class":274},[264,3055,908],{"class":270},[264,3057,3058],{"class":281},"\"Anomaly detected (",[264,3060,914],{"class":308},[264,3062,3063],{"class":281},"'above'",[264,3065,2065],{"class":270},[264,3067,2068],{"class":274},[264,3069,2199],{"class":281},[264,3071,1238],{"class":274},[264,3073,2075],{"class":270},[264,3075,2068],{"class":274},[264,3077,2244],{"class":281},[264,3079,1238],{"class":274},[264,3081,1300],{"class":270},[264,3083,3084],{"class":281}," 'below'",[264,3086,752],{"class":308},[264,3088,3089],{"class":281}," baseline).\"",[264,3091,1012],{"class":274},[264,3093,3095,3097],{"class":266,"line":3094},168,[264,3096,1191],{"class":270},[264,3098,2728],{"class":274},[264,3100,3102,3104,3106,3109],{"class":266,"line":3101},169,[264,3103,1103],{"class":308},[264,3105,957],{"class":274},[264,3107,3108],{"class":281},"\"Dry run: skipping webhook post.\"",[264,3110,1012],{"class":274},[264,3112,3114,3116],{"class":266,"line":3113},170,[264,3115,1910],{"class":270},[264,3117,3041],{"class":308},[264,3119,3121],{"class":266,"line":3120},171,[264,3122,435],{"emptyLinePlaceholder":434},[264,3124,3126],{"class":266,"line":3125},172,[264,3127,3128],{"class":274},"    post_alert(webhook_url, scope, result, currency)\n",[264,3130,3132,3134,3136,3139],{"class":266,"line":3131},173,[264,3133,2890],{"class":308},[264,3135,957],{"class":274},[264,3137,3138],{"class":281},"\"Webhook alert posted.\"",[264,3140,1012],{"class":274},[264,3142,3144,3146],{"class":266,"line":3143},174,[264,3145,1613],{"class":270},[264,3147,3041],{"class":308},[264,3149,3151],{"class":266,"line":3150},175,[264,3152,435],{"emptyLinePlaceholder":434},[264,3154,3156],{"class":266,"line":3155},176,[264,3157,435],{"emptyLinePlaceholder":434},[264,3159,3161,3163,3166,3169,3172],{"class":266,"line":3160},177,[264,3162,1289],{"class":270},[264,3164,3165],{"class":308}," __name__",[264,3167,3168],{"class":270}," ==",[264,3170,3171],{"class":281}," \"__main__\"",[264,3173,1055],{"class":274},[264,3175,3177],{"class":266,"line":3176},178,[264,3178,3179],{"class":274},"    sys.exit(main())\n",[34,3181,3183],{"id":3182},"notes","Notes",[39,3185,3186,3196,3199,3210,3216,3219,3222],{},[42,3187,3188,3191,3192,3195],{},[29,3189,3190],{},"Don't evaluate today."," An earlier draft compared \"the most recent row\" against the baseline, and the most recent row is a partial day, so it raised a false \"drop\" alert on most runs. Microsoft documents that cost data for EA and MCA subscriptions typically lands within 8 to 24 hours and can take up to 72 hours for pay-as-you-go, and its own anomaly detection waits 36 hours after the end of the UTC day. ",[46,3193,3194],{},"--settle-days 2"," mirrors that; raise it to 3 for pay-as-you-go subscriptions.",[42,3197,3198],{},"Days with no usage don't come back as rows, so the script seeds every day in the window with zero. Without that, a quiet weekend silently shortens the baseline instead of pulling the mean down.",[42,3200,3201,3202,3205,3206,3209],{},"The Query API throttles by ",[29,3203,3204],{},"query processing units"," per tenant (currently 12 per 10 seconds, 60 per minute, 600 per hour, with one QPU per month of data queried). A throttled call returns 429 and the ",[46,3207,3208],{},"x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after"," header, which the script honours. One run per scope per day is far below those limits; hundreds of resource-group scopes in a tight loop are not.",[42,3211,3212,3213,3215],{},"A threshold of 2.5 standard deviations flags about 1.2% of days (both sides combined) if daily cost were normally distributed. It usually isn't: weekly batch jobs make Mondays expensive on purpose. Either use ",[46,3214,169],{}," in multiples of seven and compare same-weekday baselines, or rely on the built-in detector, which models weekly patterns for you.",[42,3217,3218],{},"The script alerts on drops as well as spikes. A sudden drop often means a workload stopped, or a backup job failed, all worth knowing.",[42,3220,3221],{},"Costs in Cost Management are estimates until the invoice is generated, and the billing period is finalized up to 72 hours after it ends. Small day-over-day revisions near month end are normal.",[42,3223,3224,3226,3227,702,3230,3233],{},[46,3225,69],{}," picks a managed identity automatically in Azure Automation, Functions or a VM; grant that identity Cost Management Reader and no secrets are needed. The same package exposes a management SDK (",[46,3228,3229],{},"pip install azure-mgmt-costmanagement",[46,3231,3232],{},"CostManagementClient",") if you prefer typed models over raw REST.",[3235,3236,3238],"h3",{"id":3237},"turn-on-the-built-in-anomaly-alert-too","Turn on the built-in anomaly alert too",[11,3240,3241,3242,3245,3246,3249,3250,3253,3254,3257],{},"Microsoft's anomaly alerts are a scheduled action of kind ",[46,3243,3244],{},"InsightAlert",". Creating one needs Cost Management Contributor (or ",[46,3247,3248],{},"Microsoft.CostManagement\u002FscheduledActions\u002Fwrite","). Because Azure checks the creator's permissions each time it sends the email, Microsoft suggests creating it as a service principal if your users hold elevated roles only through PIM. The ",[46,3251,3252],{},"viewId"," must point at the ",[46,3255,3256],{},"ms:DailyAnomalyByResourceGroup"," view or the rule won't appear in the portal:",[255,3259,3263],{"className":3260,"code":3261,"language":3262,"meta":260,"style":260},"language-http shiki shiki-themes github-dark","PUT https:\u002F\u002Fmanagement.azure.com\u002Fsubscriptions\u002F\u003Csubscription-id>\u002Fproviders\u002FMicrosoft.CostManagement\u002FscheduledActions\u002Fdaily-anomaly-alert?api-version=2025-03-01\nContent-Type: application\u002Fjson\n\n{\n  \"kind\": \"InsightAlert\",\n  \"properties\": {\n    \"displayName\": \"Daily cost anomaly alert\",\n    \"status\": \"Enabled\",\n    \"viewId\": \"\u002Fsubscriptions\u002F\u003Csubscription-id>\u002Fproviders\u002FMicrosoft.CostManagement\u002Fviews\u002Fms:DailyAnomalyByResourceGroup\",\n    \"notification\": {\n      \"to\": [\"finops@example.com\"],\n      \"subject\": \"Cost anomaly detected\"\n    },\n    \"schedule\": {\n      \"frequency\": \"Daily\",\n      \"startDate\": \"2025-12-24T00:00:00Z\",\n      \"endDate\": \"2026-12-24T00:00:00Z\"\n    }\n  }\n}\n","http",[46,3264,3265,3270,3275,3279,3284,3289,3294,3299,3304,3309,3314,3319,3324,3329,3334,3339,3344,3349,3353,3358],{"__ignoreMap":260},[264,3266,3267],{"class":266,"line":267},[264,3268,3269],{},"PUT https:\u002F\u002Fmanagement.azure.com\u002Fsubscriptions\u002F\u003Csubscription-id>\u002Fproviders\u002FMicrosoft.CostManagement\u002FscheduledActions\u002Fdaily-anomaly-alert?api-version=2025-03-01\n",[264,3271,3272],{"class":266,"line":285},[264,3273,3274],{},"Content-Type: application\u002Fjson\n",[264,3276,3277],{"class":266,"line":298},[264,3278,435],{"emptyLinePlaceholder":434},[264,3280,3281],{"class":266,"line":438},[264,3282,3283],{},"{\n",[264,3285,3286],{"class":266,"line":444},[264,3287,3288],{},"  \"kind\": \"InsightAlert\",\n",[264,3290,3291],{"class":266,"line":450},[264,3292,3293],{},"  \"properties\": {\n",[264,3295,3296],{"class":266,"line":456},[264,3297,3298],{},"    \"displayName\": \"Daily cost anomaly alert\",\n",[264,3300,3301],{"class":266,"line":461},[264,3302,3303],{},"    \"status\": \"Enabled\",\n",[264,3305,3306],{"class":266,"line":467},[264,3307,3308],{},"    \"viewId\": \"\u002Fsubscriptions\u002F\u003Csubscription-id>\u002Fproviders\u002FMicrosoft.CostManagement\u002Fviews\u002Fms:DailyAnomalyByResourceGroup\",\n",[264,3310,3311],{"class":266,"line":473},[264,3312,3313],{},"    \"notification\": {\n",[264,3315,3316],{"class":266,"line":478},[264,3317,3318],{},"      \"to\": [\"finops@example.com\"],\n",[264,3320,3321],{"class":266,"line":484},[264,3322,3323],{},"      \"subject\": \"Cost anomaly detected\"\n",[264,3325,3326],{"class":266,"line":490},[264,3327,3328],{},"    },\n",[264,3330,3331],{"class":266,"line":496},[264,3332,3333],{},"    \"schedule\": {\n",[264,3335,3336],{"class":266,"line":501},[264,3337,3338],{},"      \"frequency\": \"Daily\",\n",[264,3340,3341],{"class":266,"line":507},[264,3342,3343],{},"      \"startDate\": \"2025-12-24T00:00:00Z\",\n",[264,3345,3346],{"class":266,"line":513},[264,3347,3348],{},"      \"endDate\": \"2026-12-24T00:00:00Z\"\n",[264,3350,3351],{"class":266,"line":519},[264,3352,889],{},[264,3354,3355],{"class":266,"line":524},[264,3356,3357],{},"  }\n",[264,3359,3360],{"class":266,"line":529},[264,3361,934],{},[11,3363,3364,3365,3368],{},"The email is sent once per anomaly, at detection, and includes the top resource group changes against the previous 60 days. When the ",[46,3366,3367],{},"endDate"," passes, the alert expires and stops sending, so put a renewal on the calendar.",[34,3370,3372],{"id":3371},"references","References",[39,3374,3375,3383,3390,3397,3404,3411,3418],{},[42,3376,3377],{},[15,3378,3382],{"href":3379,"rel":3380},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Frest\u002Fapi\u002Fcost-management\u002Fquery\u002Fusage",[3381],"nofollow","Query - Usage (Cost Management REST API)",[42,3384,3385],{},[15,3386,3389],{"href":3387,"rel":3388},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fmanage-automation",[3381],"Manage Azure costs with automation (data latency, QPU rate limits)",[42,3391,3392],{},[15,3393,3396],{"href":3394,"rel":3395},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Funderstand\u002Fanalyze-unexpected-charges",[3381],"Identify anomalies and unexpected changes in cost",[42,3398,3399],{},[15,3400,3403],{"href":3401,"rel":3402},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Frest\u002Fapi\u002Fcost-management\u002Fscheduled-actions\u002Fcreate-or-update-by-scope",[3381],"Scheduled Actions - Create Or Update By Scope",[42,3405,3406],{},[15,3407,3410],{"href":3408,"rel":3409},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Funderstand-cost-mgt-data",[3381],"Understand Cost Management data (update frequency and finalization)",[42,3412,3413],{},[15,3414,3417],{"href":3415,"rel":3416},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fassign-access-acm-data",[3381],"Assign access to Cost Management data",[42,3419,3420],{},[15,3421,3424],{"href":3422,"rel":3423},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpython\u002Fapi\u002Foverview\u002Fazure\u002Fmgmt-costmanagement-readme",[3381],"Azure Cost Management client library for Python",[3426,3427,3428],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":260,"searchDepth":285,"depth":285,"links":3430},[3431,3432,3433,3434,3435,3438],{"id":36,"depth":285,"text":37},{"id":113,"depth":285,"text":114},{"id":249,"depth":285,"text":250},{"id":412,"depth":285,"text":413},{"id":3182,"depth":285,"text":3183,"children":3436},[3437],{"id":3237,"depth":298,"text":3238},{"id":3371,"depth":285,"text":3372},"techcolumnist",[3441,3442],"scripts","engineering","2025-12-24T14:00:00Z","A Python script that pulls daily Azure costs from the Cost Management Query API and posts a webhook alert when a day breaks from its rolling baseline.","md",false,null,{},"\u002Fblog\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api",{"title":6,"description":3444},[3439],"blog\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api",[302,3454],"azure","\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api\u002F","6OFc1RLVtO7qbEqkKjD-1RpOSeaqbtBEEIsk_R4s3KA",{"title":3458,"description":3459,"date":3460,"url":3461,"categories":3462,"tags":3463,"image":3447,"readingTime":298,"canonical":3439,"sites":3466,"series":3447,"seriesOrder":3447},"Python: Discovery – Fingerprint Every Device on a Subnet","A Python script that ARP-sweeps a local subnet with Scapy, maps MACs to IEEE vendors, probes common ports for banners, and writes one CSV row per device.","2026-01-07T14:00:00Z","\u002F2026\u002F01\u002F07\u002Fpython-discovery-fingerprint-every-device-on-a-subnet\u002F",[3441,3442],[302,3464,3465],"discovery","network",[3439],{"title":3468,"description":3469,"date":3470,"url":3471,"categories":3472,"tags":3473,"image":3447,"readingTime":444,"canonical":3439,"sites":3478,"series":3447,"seriesOrder":3447},"Python: Data Warehouse – Incremental Loads from Postgres to BigQuery","A Python script that copies new and changed Postgres rows into BigQuery with one load job and a MERGE, advancing the watermark in the same transaction.","2025-12-17T14:00:00Z","\u002F2025\u002F12\u002F17\u002Fpython-data-warehouse-incremental-loads-from-postgres-to-bigquery\u002F",[3441,3442],[302,3474,3475,3476,3477],"data-warehouse","etl","gcloud","sql",[3439],[3480,3487,3497],{"title":23,"description":3481,"date":3482,"url":22,"categories":3483,"tags":3484,"image":3447,"readingTime":444,"canonical":3439,"sites":3486,"series":3447,"seriesOrder":3447},"A Node.js script that queries month-to-date Azure cost per resource group, projects month-end spend, and posts Block Kit messages to Slack for groups over budget.","2026-02-18T14:00:00Z",[3441,3442],[3485,3454],"nodejs",[3439],{"title":3488,"description":3489,"date":3490,"url":3491,"categories":3492,"tags":3493,"image":3447,"readingTime":444,"canonical":3439,"sites":3496,"series":3447,"seriesOrder":3447},"Python: Talos – Health-Checking a Kubernetes Cluster from a Cron Job","A cron-friendly Python health check for Talos clusters: talosctl health, etcd alarms, node readiness, cordons and crash-looping pods, with a webhook alert.","2026-02-04T14:00:00Z","\u002F2026\u002F02\u002F04\u002Fpython-talos-health-checking-a-kubernetes-cluster-from-a-cron-job\u002F",[3441,3442],[302,3494,3495],"talos","kubernetes",[3439],{"title":3498,"description":3499,"date":3500,"url":3501,"categories":3502,"tags":3503,"image":3447,"readingTime":298,"canonical":3439,"sites":3505,"series":3447,"seriesOrder":3447},"Python: Discovery – Crawling DNS Zones for Orphaned Records","A dnspython script that transfers a zone with AXFR, probes every A\u002FAAAA record for signs of life, flags dangling CNAMEs, and reports orphan cleanup candidates.","2026-01-28T14:00:00Z","\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records\u002F",[3441,3442],[302,3464,3465,3504],"windows",[3439],{"doc":3447,"posts":3507},[],1790052513392]