AWS Python(Boto3)の例外ハンドリングとテスト時のraise方法

Lambdaなど、AWSリソースを使用した開発をしているとboto3のエラーハンドリングをする機会が必ず出てきます。
また、Pytestなどテスト自動化を導入している際にraiseさせたい場面もよくあります。
そこで、エラーハンドリングをする際の手順を簡単にまとめました。

## ハンドリング方法
- DynamoDBへレコード追加
- ID重複時に重複している旨を出力する

という想定で行っていきます。

```Python:エラーハンドリング
import boto3
from botocore.exceptions import ClientError
# boto3のエラーを司るClientErrorをimportしておく

dynamodb_client = boto3.client('dynamodb')


def main(table_name, id_):
    param = {
        "TableName": table_name,
        "Item": {
            "id": {"S": id_}
        },
        "Expected": {
            "id": {
                "Exists": False
            }
        }
    }

    try:
        dynamodb_client.put_item(**param)
    except ClientError as e:
        # ClientErrorをキャッチするようにする
        # エラー内容からCodeを抜き取り比較する。重複時にはConditionalCheckFailedExceptionが返ってくる。
        if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
            print("IDが重複しています。")
            return

        # 重複以外はエラー
        raise e


if __name__ == "__main__":
    main('hogehoge', 'fugafuga')
```

一回目は正常に動きますが、2回目は`IDが重複しています。`とメッセージが出ます。

### ClientErrorのエラー内容を確認

VSCodeのデバッガを使用して中身を確認してみました。

![screenshot_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165191/f79f316d-7d14-fa4d-eaf4-5484b78323c5.png)

御覧の通り、response内にErrorのキーがあり、さらにCodeのキーがあるのが確認できると思います。
エラーコード自体はAWSの公式リファレンスに載っていますので、事前に確認しておくのが良いと思います。
[AWS Dynamodb | エラー処理](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Programming.Errors.html)

## raise方法

ここまでは他にもたくさんqiitaの記事がありますが、raiseさせる方法を記述している記事はなかなか無かったのでメモがてら。

結論から言うとraiseするClientErrorに引数を渡してあげます。

```Python:try内でraiseさせる
# 先ほどのコードのtry,except部分のみ抜粋です
    try:
        # "Error"キーと"Code"キーの辞書型を用意
        # "Code"にはハンドリングさせたいエラーコードを入力
        error_response = {
            "Error": {
                "Code": "ConditionalCheckFailedException"
            }
        }
        # 文字列です。関数名などを格納する？
        operation_name = "put_item"
        raise ClientError(error_response, operation_name)
        dynamodb_client.put_item(**param)
    except ClientError as e:
        # ClientErrorをキャッチするようにする
        # エラー内容からCodeを抜き取り比較する。重複時にはConditionalCheckFailedExceptionが返ってくる。
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print("IDが重複しています。")
            return
```

これで`raise`できます。

### raise ClientErrorのエラー内容を確認

![screenshot_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165191/e9432371-d93b-a440-7938-70f75b5c0c46.png)

若干メンバが足りないようですが、動作確認などには十分ですね。

### ClientErrorの実態

ClientErrorはどのような構成になっているのか見てみました。

```Python:ClientError
class ClientError(Exception):
    MSG_TEMPLATE = (
        'An error occurred ({error_code}) when calling the {operation_name} '
        'operation{retry_info}: {error_message}')

    def __init__(self, error_response, operation_name):
        retry_info = self._get_retry_info(error_response)
        error = error_response.get('Error', {})
        msg = self.MSG_TEMPLATE.format(
            error_code=error.get('Code', 'Unknown'),
            error_message=error.get('Message', 'Unknown'),
            operation_name=operation_name,
            retry_info=retry_info,
        )
        super(ClientError, self).__init__(msg)
        self.response = error_response
        self.operation_name = operation_name

(続く)...
```

こちらがClientErrorの中身です。`__init__`で2つの引数を受け取ることができるのが読み取れます。
第1引数の`error_response`は辞書型であり、Errorがありその下にCodeが存在していればいいみたいです。