boto3のClientAPIのレスポンスをデータクラスで定義する

# はじめに
お勉強の為にboto3を弄ってました。
boto3を利用して得たリソースの情報はデータクラスに格納して参照した方がいい(理由は後述)と考えたので知見として記事にしときます。

# boto3ってなんだ!?
簡潔に説明するとpythonでAWSリソースを操作するためのSDKです。
低レベル(client)APIと高レベルAPI(resorce)に分別されます。
関連記事は他に無限にあるので、詳しく知りたい方はそちらをどうぞ。

今回は低レベルAPIを使用します。

# データクラスってなんだ!?
\_\_init\_\_()等のクラスに紐づく特殊なメソッドを動的に付与してくれるデコレータを提供してくれます。

以下のようなコンストラクタを持ったクラスを

```py
class Wanchan_Nekochan():
    def __init__(self, cat:str, dog:str):
    　self.cat =cat
      self.dog = dog
```

このようなコンストラクタが無いスマートな記述ができるようになります

```py
@dataclass
class Wanchan_Nekochan():
    cat: str
    dog: str
```



https://docs.python.org/ja/3/library/dataclasses.html

類似機能を持ったnamedtupleと比較する場合、以下のような差別点があります。
・namedtupleはインスタンス生成後にイミュータブル(編集不可)状態となる。
dataclassはデフォルトではミュータブル(編集可)だが、引数のfrozenをtrueで渡すと
イミュータブルとなる。
・dataclassはデータの読み取りが少し早い(要検証)

# 演習
今回はS3のバケット一覧を取得する為、S3.Clientクラスのlist_buckets()メソッドを使用します。
(参考)公式リファレンス
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.list_buckets

レスポンスの定義は以下の辞書型です。

```
{
    'Buckets': [
        {
            'Name': 'string',
            'CreationDate': datetime
        },
    ],
    'Owner': {
        'DisplayName': 'string',
        'ID': 'string'
    },
    "ResponseMetadata": {
        "RequestId": str,
        "HTTPStatusCode": int,
        "HTTPHeaders": dict,
        "RetryAttempts": int
    }
}
```

帰ってきたレスポンスから情報を取得する際には以下のようにキーを文字列で指定することになります。

```py
s3_client = session.client('s3')
data=s3_client.list_buckets()

status_code = data["ResponseMetadata"]["HTTPStatusCode"]
display_name = data["Owner"]["DisplayName"]
```

文字列によるキー指定の為、IDEによる入力補完機能が使えないのでスペルミスの危険性がある、そのデータの持つ型が何か参照するまでわからない等の困った問題が存在します。

予めレスポンスの定義をdataclassとして定義しておけばドットアクセスが可能なので、入力補完が可能になったり、mypyによる型ヒントをゴリッゴリに利用できるようになります。

こりゃdataclass使わない理由ないっすね!!!

という事で...

## とりあえずソース
```py
from dataclasses import dataclass
from typing import List
from datetime import datetime

import boto3
from dacite import from_dict


session = boto3.session.Session(profile_name='s3_test')
# BasesClientは何回もAPI投げるのを避ける為グローバルスコープに置いて
# シングルトンで実装する
s3_client = session.client('s3')

@dataclass
class Boto3_Response():
    RequestId: str
    HostId: str
    HTTPStatusCode: int
    HTTPHeaders: Dict
    RetryAttempts: int

@dataclass
class Inner_Owner():
    DisplayName: str
    ID: str

@dataclass
class Inner_Buckets():
    Name: str
    CreationDate: datetime

@dataclass
class S3_LIST():
    ResponseMetadata: Boto3_Response
    Owner: Inner_Owner
    Buckets: List[Inner_Buckets]

    @classmethod
    def make_s3_name_list(cls):
        return from_dict(data_class=cls, data=s3_client.list_buckets())


s3_list_response = S3_LIST.make_s3_name_list()

# ステータスコード
print(s3_list_response.ResponseMetadata.HTTPStatusCode) #200
# オーナー名
print(s3_list_response.Owner.DisplayName) # nikujaga-kun
# ID
print(s3_list_response.Owner.ID) 
# バケット一覧
print(*[bucket.Name for bucket in s3_list_response.Buckets])
```

# 以下解説
```py
from dacite import from_dict
```
ここでdaciteなるサードパーティーライブラリをインポートしています。
dacite(筆者は"でして"と読んでます)は簡潔に言うとネストしたデータクラスに辞書型を渡してインスタンス生成する為のライブラリです。
https://pypi.org/project/dacite/

```py
@dataclass
class Boto3_Response():
    RequestId: str
    HostId: str
    HTTPStatusCode: int
    HTTPHeaders: Dict
    RetryAttempts: int

@dataclass
class Inner_Owner():
    DisplayName: str
    ID: str

@dataclass
class Inner_Buckets():
    Name: str
    CreationDate: datetime

@dataclass
class S3_LIST():
    ResponseMetadata: Boto3_Response
    Owner: Inner_Owner
    Buckets: List[Inner_Buckets]

    @classmethod
    def make_s3_name_list(cls):
        return from_dict(data_class=cls, data=s3_client.list_buckets())
```

上記S3_LISTの定義において、別のデータクラスであるBoto3_Response,Inner_Owner,Inner_Bucketsを属性の型としてそれぞれ定義しています。
daciteのfrom_dictを利用せずに、そのままlist_bucketsのレスポンスをアンパックでS3_LISTに渡すと

```py
@classmethod
   def make_s3_name_list(cls):
      return cls(**s3_client.list_buckets())
```

このような形になるわけですが、クラスインスタンスでなく辞書型で渡している為そんな属性無いと怒られます。

```py
s3_list_response = S3_LIST.make_s3_name_list()

# ここでAttributeErrorで怒られる
print(s3_list_response.ResponseMetadata.HTTPStatusCode)

# 'dict' object has no attribute 'HTTPStatusCode'
```
組込だけでここらへんを上手いことやろうとすると、それなりに大変なので、
よしなにやってくれるライブラリとしてdaciteを利用しています。
巨人の肩には無限に乗るべき。


# 最後に
dataclassはいいぞ