Python で Azure の 使用料金一覧表をTeamsに Post(Webhook) してみました


## 概要

[この記事](https://qiita.com/turupon/items/4e078450a6040dcec843) のPython プログラムを変更し、Microsoft Teams に サブスクリプション毎の使用料金合計一覧表と日毎の使用料金一覧表（Teamsで水平スクロールができないので、２つに分割）を１つのText項目として Post(Webhook) してみました。 Postするにあたり、データは markdown 形式に変換しています（html形式にもトライしましたが、大変見にくかった）。

### ローカル環境

macOS Monterey 12.1
python 3.8.12

### 実行プログラム

```python:GetSubscriptionCostManagement_23.py
import os
import json
import requests
import time
import argparse
from azure.identity import AzureCliCredential, DefaultAzureCredential
from azure.mgmt.resource import SubscriptionClient
from azure.mgmt.costmanagement import CostManagementClient
import pandas as pd
from datetime import datetime, date, timedelta


# teams_endpoint = 'Microsoft Teamsの チャネルエンドポイント(Webhook)'
TEAMS_ENDPOINT = {
    "TECH_TEST": os.environ['ENDPOINT_TECH_TEST']
}


# 接続しているテナントのサブスクリプションを操作するオブジェクトを取得
def GetSubscriptionObject():
    subscription_client = SubscriptionClient(
        # credential=AzureCliCredential()
        credential=DefaultAzureCredential()
    )
    return subscription_client


# CostManagement情報 を操作するオブジェクトを取得
def GetCostManagementObject():
    costmgmt_client = CostManagementClient(
        # credential=AzureCliCredential()
        credential=DefaultAzureCredential()
    )
    return costmgmt_client


# 指定した Subscription について CostManagement からコストを取得
def GetCostManagement(costmgmt_client, subs_id):

    # Query costmanagement
    SCOPE = '/subscriptions/{}'.format(subs_id)
    costmanagement = costmgmt_client.query.usage(
        SCOPE,
        {
            "type": "Usage",
            "timeframe": "MonthToDate",
            "dataset": {
                "granularity": "Daily",
                "aggregation": {
                    "totalCost": {
                        "name": "PreTaxCost",
                        "function": "Sum"
                    }
                },
                "grouping": [
                    {
                        "type": "Dimension",
                        "name": "ResourceGroup"
                    }
                ]
            }
        }
    )
    return costmanagement


# 合計行の追加
def append_sum_row_label(df):
    df.loc['Total'] = df.sum(numeric_only=True)
    return df


# 日単位のサブスクリプション毎の一覧表
def Report_Daily(df):
    # Teamsで水平スクロールができないのでデータを２つに分割
    df1 = df.loc[:,['jjj', 'eee', 'bbb', 'ggg', 'ffff', 'ddd', 'hhh', 'ccc']]
    df2 = df.loc[:,['japp', 'iapp', 'aaa', 'kkk', 'MixixTeam', 'iii']]
    # reset_indexする
    df1.reset_index(inplace=True)
    df2.reset_index(inplace=True)
    # 合計行の追加
    df1 = append_sum_row_label(df1)
    df2 = append_sum_row_label(df2)

    # html形式への変換例
    # post_str1 = df1.to_html(float_format='{:,.0f}'.format)

    # markdown形式への変換
    post_str1 = df1.astype(str).to_markdown(floatfmt=",.0f")
    post_str2 = df2.astype(str).to_markdown(floatfmt=",.0f")
    print("\n------------------------------------------------\n")
    print(post_str1)
    print("\n------------------------------------------------\n")
    print(post_str2)
    return post_str1, post_str2


# サブスクリプションの合計一覧表（利用料金での降順）
def Report_Summury(tdf):
    # 利用料金で降順のソート
    tdf = tdf.sort_values(by=['UsageCost'], ascending=False)
    # カラムの順番を変更
    tdf = tdf.loc[:,['Subscription', 'UsageCost']]
    # reset_indexする
    tdf.reset_index(inplace=True)
    # 不要カラムの削除
    tdf = tdf.drop(['Date'], axis=1)
    # markdown形式への変換
    post_str0 = tdf.to_markdown(floatfmt=",.0f")
    print("\n------------------------------------------------\n")
    print(post_str0)
    return post_str0


# TeamsEndpointへのデータPOST
def teams_endpoint_post(post_str1, post_str2, post_str0, totalcost):
    # 前日の日付の取得
    day0 = (date.today() + timedelta(days=-1)).isoformat()

    # Microsoft Teams へ送信する下ごしらえ
    request = {
        'title':  '【 Azure : ' + day0 + ' 】 合計： ¥ ' + totalcost,
        'text': post_str0 + '\n\n' + post_str1 + '\n\n' + post_str2
    }

    # Microsoft Teams へ送信する
    response = requests.post(TEAMS_ENDPOINT['TECH_TEST'], json.dumps(request))
    print(response)


# サブスクリプションIDを指定しリソースグループ毎に CostManagement情報を取得
def GetSubscriptionCsotManagement():
    
    # サブスクリプションを操作するオブジェクトの取得
    subscription_list = GetSubscriptionObject()    

    # CostManagementを操作するオブジェクトの取得
    costmgmt_client = GetCostManagementObject()

    # 取得項目の定義
    row_list = ['UsageCost', 'Date', 'ResourceGroup', 'Currency']

    # 小数点以下の桁数：2（デフォルト：6）＋コンマ区切りの挿入 を定義
    pd.options.display.float_format = '{:,.2f}'.format
    
    # サブスクリプション毎に CostManagement からコストを取得
    for n, subs in enumerate(subscription_list.subscriptions.list()):
        # print("\nサブスクリプション ： {}  {}".format(subs.subscription_id, subs.display_name))
        costmanagement = GetCostManagement(costmgmt_client, subs.subscription_id)

        # 表示させるデータのDataFrame化
        if len(costmanagement.rows) > 0 :
            # DataFrame型でデータの取得
            rowdf = pd.DataFrame(costmanagement.rows, columns = row_list)
            # 不必要項目の削除
            rowdf = rowdf.drop(['Currency', 'ResourceGroup'], axis=1)
            # Date項目の型をdatetime型に変換
            rowdf['Date'] = pd.to_datetime(rowdf['Date'].astype(str), format='%Y-%m-%d')

            # インデックス化
            rowdf.set_index('Date', inplace=True)
            # resampleメソッドで、日単位で集計
            dsumdf = rowdf.resample('D').sum()
            # resampleメソッドで、月単位で集計
            msumdf = rowdf.resample('M').sum()
            msumdf['Subscription'] = subs.display_name

            print("\tサブスクリプション : {} ".format(subs.display_name))
                
            if n == 0 :
                df = dsumdf
                df.rename(columns={'UsageCost': subs.display_name}, inplace=True)
                tdf = msumdf
            else :
                df[subs.display_name] = dsumdf['UsageCost']
                tdf = tdf.append(msumdf)
    
    # オブジェクトのクローズ処理
    costmgmt_client.close()
    subscription_list.close()

    # 日単位のサブスクリプション毎の一覧表
    post_str1, post_str2 = Report_Daily(df)

    ### サブスクリプションの合計一覧表（利用料金での降順）
    post_str0 = Report_Summury(tdf)

    # resampleメソッドで、月単位で集計
    totalcost = tdf['UsageCost'].sum()

    # TeamsEndpointへのデータPOST
    teams_endpoint_post(post_str1, post_str2, post_str0, '{:,.0f}'.format(round(totalcost)))


if __name__ == '__main__':
    
    parser = argparse.ArgumentParser(description='Subscription の 日々のコスト取得')
    args = parser.parse_args()

    start = time.time()
    GetSubscriptionCsotManagement()
    generate_time = time.time() - start

    print("\n 取得時間:{0}".format(generate_time) + " [sec] \n")
```

### プログラムの実行
 
```
$ python GetSubscriptionCostManagement_23.py
	サブスクリプション : iapp
	サブスクリプション : japp 
	サブスクリプション : aaa 
	サブスクリプション : bbb 
	サブスクリプション : ccc 
	サブスクリプション : ddd 
	サブスクリプション : eee 
	サブスクリプション : fff 
	サブスクリプション : ggg 
	サブスクリプション : hhh 
	サブスクリプション : iii 
	サブスクリプション : jjj 
	サブスクリプション : kkk 
	サブスクリプション : MixixTeam 

------------------------------------------------

|    | Subscription   |   UsageCost |
|---:|:---------------|------------:|
|  0 | bbb            |     144,455 |
|  1 | ddd            |      75,169 |
|  2 | cccc           |      43,630 |
|  3 | aaa            |      41,474 |
|  4 | ggg            |      36,264 |
|  5 | kkk            |      34,875 |
|  6 | iapp           |      22,780 |
|  7 | hhh            |      18,500 |
|  8 | fff            |      18,066 |
|  9 | japp           |       8,204 |
| 10 | jjj            |       6,682 |
| 11 | eee            |       5,174 |
| 12 | MixedTeam      |       3,294 |
| 13 | iii            |       2,899 |

------------------------------------------------

|       | Date       |      jjj |      eee |       bbb |       ggg |       fff |       ddd |      hhh |      ccc |
|:------|:-----------|---------:|---------:|----------:|----------:|----------:|----------:|---------:|---------:|
| 0     | 2022-03-01 |      192 |      192 |     4,579 |     1,521 |       314 |    13,840 |      473 |    1,619 |
| 1     | 2022-03-02 |      429 |      192 |     5,066 |     1,524 |       314 |     3,839 |      473 |    1,619 |
| 2     | 2022-03-03 |      657 |      192 |     6,022 |     1,863 |       314 |     3,839 |      530 |    1,619 |
| 3     | 2022-03-04 |      828 |      192 |     5,779 |     1,699 |       314 |     3,839 |      512 |    1,619 |
| 4     | 2022-03-05 |      221 |      192 |     4,541 |       854 |       314 |     3,839 |      509 |    1,619 |
| 5     | 2022-03-06 |      221 |      192 |     4,539 |       846 |       314 |     3,839 |      509 |    1,620 |
| 6     | 2022-03-07 |      262 |      192 |     4,665 |       848 |       307 |     3,673 |      509 |    1,619 |
| 7     | 2022-03-08 |      221 |      192 |     5,718 |     3,476 |         5 |     1,099 |      509 |    1,620 |
| 8     | 2022-03-09 |      195 |      192 |     6,617 |     2,500 |         5 |     1,099 |      507 |    1,619 |
| 9     | 2022-03-10 |      192 |      192 |     6,696 |     2,059 |     6,045 |     1,099 |      763 |    1,619 |
| 10    | 2022-03-11 |      192 |      192 |     2,060 |     1,311 |     1,470 |     1,099 |      709 |    1,619 |
| 11    | 2022-03-12 |      192 |      192 |     6,379 |     1,017 |       914 |     1,099 |      540 |    1,619 |
| 12    | 2022-03-13 |      189 |      192 |     6,370 |     1,018 |       914 |     1,099 |      540 |    1,620 |
| 13    | 2022-03-14 |      192 |      192 |     7,036 |     1,019 |       914 |     1,099 |      540 |    1,619 |
| 14    | 2022-03-15 |      189 |      192 |     6,726 |     1,499 |       914 |     1,098 |      540 |    1,619 |
| 15    | 2022-03-16 |      192 |      192 |     5,143 |     1,516 |     2,807 |     1,098 |      540 |    1,619 |
| 16    | 2022-03-17 |      192 |      192 |     6,952 |     3,141 |     1,839 |     1,095 |      540 |    1,620 |
| 17    | 2022-03-18 |      192 |      192 |     7,104 |     1,815 |         5 |     1,095 |      509 |    1,619 |
| 18    | 2022-03-19 |      192 |      192 |     3,860 |       774 |         5 |     1,095 |      508 |    1,619 |
| 19    | 2022-03-20 |      189 |      192 |     3,846 |       602 |         5 |     1,095 |      508 |    1,620 |
| 20    | 2022-03-21 |      188 |      192 |     3,841 |       600 |         5 |     1,095 |      508 |    1,618 |
| 21    | 2022-03-22 |      192 |      192 |     8,337 |       685 |         5 |     1,095 |    1,082 |    1,620 |
| 22    | 2022-03-23 |      191 |      192 |     2,190 |       823 |         5 |     1,094 |    1,523 |    1,619 |
| 23    | 2022-03-24 |      191 |      192 |     3,723 |       957 |         5 |     1,095 |    1,156 |    1,620 |
| 24    | 2022-03-25 |      223 |      192 |     8,700 |     1,034 |         5 |     2,923 |    1,186 |    1,619 |
| 25    | 2022-03-26 |      191 |      192 |     4,102 |       646 |         5 |     3,568 |    1,163 |    1,620 |
| 26    | 2022-03-27 |      183 |      184 |     3,865 |       617 |         5 |    13,323 |    1,114 |    1,527 |
| Total | NaT        |    6,682 |    5,174 |   144,455 |    36,264 |    18,066 |    75,169 |   18,500 |   43,630 |

------------------------------------------------

|       | Date       |        japp |      iapp |             aaa |        kkk |   MixixTeam |            iii |
|:------|:-----------|------------:|----------:|----------------:|-----------:|------------:|---------------:|
| 0     | 2022-03-01 |         311 |       596 |           1,619 |        727 |           1 |            102 |
| 1     | 2022-03-02 |         312 |       597 |           1,619 |        727 |           1 |            102 |
| 2     | 2022-03-03 |         311 |       597 |           1,619 |        727 |           1 |            102 |
| 3     | 2022-03-04 |         302 |       598 |           1,626 |        727 |           1 |            102 |
| 4     | 2022-03-05 |         296 |       599 |           1,619 |        727 |           1 |            102 |
| 5     | 2022-03-06 |         297 |     1,270 |           1,619 |        727 |           1 |            102 |
| 6     | 2022-03-07 |         309 |       741 |           1,619 |        854 |           1 |            102 |
| 7     | 2022-03-08 |         309 |       710 |           1,619 |      1,294 |       2,078 |            102 |
| 8     | 2022-03-09 |         309 |       711 |           1,619 |        727 |       1,188 |            102 |
| 9     | 2022-03-10 |         309 |       716 |           1,620 |        727 |           1 |            102 |
| 10    | 2022-03-11 |         301 |       712 |           1,446 |        727 |           1 |            102 |
| 11    | 2022-03-12 |         296 |       713 |           1,260 |        727 |           1 |            102 |
| 12    | 2022-03-13 |         296 |       713 |           1,257 |        727 |           1 |            102 |
| 13    | 2022-03-14 |         308 |       710 |           1,260 |        727 |           1 |            102 |
| 14    | 2022-03-15 |         310 |       710 |           1,257 |        727 |           1 |            243 |
| 15    | 2022-03-16 |         311 |       711 |           1,581 |        727 |           1 |            102 |
| 16    | 2022-03-17 |         310 |       711 |           1,572 |        727 |           1 |            102 |
| 17    | 2022-03-18 |         302 |     1,011 |           1,573 |        727 |           1 |            102 |
| 18    | 2022-03-19 |         297 |     1,082 |           1,573 |        727 |           1 |            102 |
| 19    | 2022-03-20 |         297 |     1,082 |           1,570 |        727 |           1 |            102 |
| 20    | 2022-03-21 |         297 |     1,079 |           1,568 |        727 |           1 |            102 |
| 21    | 2022-03-22 |         310 |     1,079 |           1,573 |        802 |           1 |            102 |
| 22    | 2022-03-23 |         310 |     1,080 |           1,574 |        733 |           1 |            102 |
| 23    | 2022-03-24 |         309 |     1,080 |           1,573 |      1,594 |           1 |            102 |
| 24    | 2022-03-25 |         302 |     1,081 |           1,573 |     13,429 |           1 |            102 |
| 25    | 2022-03-26 |         297 |     1,082 |           1,573 |      1,395 |           1 |            102 |
| 26    | 2022-03-27 |         285 |     1,010 |           1,492 |        960 |           1 |             98 |
| Total | NaT        |       8,204 |    22,780 |          41,474 |     34,875 |       3,294 |          2,899 |
<Response [200]>

 取得時間:99.73083806037903 [sec] 
```

### TeamsへのPost（投稿）結果

- 以下の内容で投稿されます
  - タイトル：
    - 【 Azure : yyyy-mm-dd 】 合計： ¥ xxx,xxx
  - 本文：
    - 上記アウトプットと同じ内容（ただし、３つの表の間は改行は１つのみで少し見にくいです）

---

### まとめ

Teamsへのmarkdown形式でのPostにおいて、「改行が１行しか有効にならない」「水平スクロールができない」状況です。どなたかこの２の解決策をご存知でしたらご教示ください。