【Python】doctestでdictを扱う

課題

テストの自動化にdoctestは良いな、と思っていたらjsonのテストを行うとテストが通らない問題が発生した。

from datetime import datetime,timedelta
import json

def testFunc(test):
    """
   >>> testFunc("Hello!")
    {"test":"Hello!"}
    """
    
    return {"test":test}
Failed example:
    testFunc("Hello!")
Expected:
    {"test":"Hello!"}
Got:
    {'test': 'Hello!'}
1 items had no tests:
    test
**********************************************************************
1 items had failures:
   1 of   1 in test.testFunc
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

解決方法

以下のようにすると良いらしい。

#method1
>>> sorted(testFunc("Hello!").items())
[("test","Hello!")]

又は

#method2
>>> testFunc("Hello!")== {"test":"Hello!"}
True

なお、method2は期待する出力と関数内での出力の順番が異なっていても問題なくテストは通る

from datetime import datetime,timedelta
import json

def func(test):
    """
    >>> testFunc("Hello!")=={"hello":"World","test":"Hello!"}
    True
    """
    
    return {
        "test":test",
        "hello":"World"
    }

流石に改行を入れるとテストコードが通らないが、きちんとバックスラッシュ()を入れれば改行やインデントも入れられるため、可読性を上げられる。

#NG
>>> testFunc("Hello!")=={
            "test":"Hello!",
            "hello":"World"
        }
    True

#OK
>>> testFunc("Hello!")=={\
            "test":"Hello!",\
            "hello":"World"\
        }
    True
#NG
Failed example:
    testFunc("Hello!")=={
Exception raised:
    Traceback (most recent call last):
      File "C:\ProgramData\Anaconda3\lib\doctest.py", line 1329, in __run
        compileflags, 1), test.globs)
      File "<doctest test.testFunc[0]>", line 1
        testFunc("Hello!")=={
                            ^
    SyntaxError: unexpected EOF while parsing
1 items had no tests:
    test
**********************************************************************
1 items had failures:
   1 of   1 in test.testFunc
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

#OK
Trying:
    testFunc("Hello!")=={            "test":"Hello!",            "hello":"World"        }
Expecting:
    True
ok
1 items had no tests:
    test
1 items passed all tests:
   1 tests in test.testFunc
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

原因

doctestはあくまで、出力が完全一致しているか確認するだけで、等価性のチェックは行わないらしい。 例えば、以下の値は並び順が異なるが本来は同じ値である。

#Example1
{
  "key":"Hello ",
  "value":"World"
}
#Example2
{
  "value":"World!",
  "key":"Hello "
}

しかし、pytdocでのテストでは並び順が異るためエラーと認識される

どうやら、課題で挙げた問題はこの延長線上にあるようだ。

所感

doctestはドキュメントをコード内に書きながらテストコードを書けるため便利だが、大きなdictやlistが出力されるコードではunittestのほうが使い勝手が良いかもしれない。 ただ、現状はそこまで大きな出力を扱うコードを書く予定がないので、まずはdoctestでドキュメントを書きながらテストコードを充実させていくことを主眼に置きたい。

参考

stackoverflow.com

【CloudFormation】S3のバケットの中身をCloudFormationで作成する

はじめに

CloudFormationでシステムのようなものを組んでいるうちに、本当で適当で良いから入力フォームみたいなものが必要になってきた。 こうしたときに便利なのがS3の静的ホスティング機能だろう。

qiita.com

ただ、CloudFormationは、S3の中身までは作成できないという欠点があった。

強力なS3Objectsマクロ

しかし、このS3Objectsを利用すれば、CloudFormationでS3内のファイルを作成することができる github.com

デプロイ

使い方は非常に簡単で、上記のサイトの手順に従ってマクロをデプロイする。適当なアーティファクトバケットを作成する。 次に、git cloneかファイルのダウンロードし*1、以下のコマンドを実行する。

aws cloudformation package \
    --template-file macro.template \
    --s3-bucket <your bucket name> \
    --output-template-file packaged.template

aws cloudformation deploy \
    --stack-name s3objects-macro \
    --template-file packaged.template \
    --capabilities CAPABILITY_IAM

これで、デプロイしたアカウント内ならばこのマクロを利用することができるようになった。 なお、私はコマンドをうつのが面倒だったため、CodeStarを利用した。

テスト

テスト用のコードも用意されているため、実行して有効性を確認する。

aws cloudformation deploy \
    --stack-name s3objects-macro-example \
    --template-file example.template \
    --capabilities CAPABILITY_IAM

使い方

CloudFormationが利用するIAMロールにS3Fullをアタッチし*2、CloudFormationテンプレートの頭に以下のブロックを置くことで、マクロが有効化される。

2019/12/21追記:マクロを動作させるためには、CloudFormationが利用するRoleにAWSLambdaExecuteポリシーをアタッチする必要がある。

Transform: S3Objects

サーバーレス変換など、他のマクロを合わせて利用する場合は、以下のようにリスト表記する。

Transform:
- S3Objects
- AWS::Serverless-2016-10-31

公式ドキュメントによると、以下の使い方があるようだ。 なお、いずれの項目でも

Target:
  Bucket: 
  Key:

は必須項目で、アップロード先のバケットとキー名を指定している。

テキストの内容をベタ書き

Transform: S3Objects
Resources:
  Bucket:
    Type: AWS::S3::Bucket

  Object:
    Type: AWS::S3::Object
    Properties:
      Target:
        Bucket: !Ref Bucket
        Key: README.md
        ContentType: text/plain
      Body: Hello, world!

Body以下に書かれた内容がそのままファイルの内部に反映される。 非常に短いコードならば問題にならないが、長文になると修正も非常に面倒になるほか、コードを変更した際にテストが行えない点に注意すること。

エンコードされた文字列を記載する

SinglePixel:
  Type: AWS::S3::Object
  Properties:
    Target:
      Bucket: !Ref TargetBucket
      Key: 1pixel.gif
    Base64Body: R0lGODdhAQABAIABAP///0qIbCwAAAAAAQABAAACAkQBADs=

ここでは、Base64の文字列をテンプレート中に記載する。

S3にアップロードされているファイルを指定する。

CopiedObject:
  Type: AWS::S3::Object
  Properties:
    Source:
      Bucket: !Ref SourceBucket
      Key: index.html
    Target:
      Bucket: !Ref TargetBucket
      Key: index.html
      ACL: public-read

予め他のS3にアップロードしておいたファイルを指定してコピーする。 aws-cliaws s3 cp ~~~をcloudFormationで実行していると考えて良いだろう。

結論

S3Objects変換を利用することで、S3バケットの中身をCloudFormationで作成することができるようになった。 工夫すれば、静的サイトをCloudFormationで作成することができるようになると考えられる。 ただし、アップロードする方法がテキストのベタ書き、または他のs3オブジェクトへのリンクのみという弱点もある。*3

*1:いずれも、他のマクロテンプレートなどが大量に入っているため時間がかかる点に注意

*2:何もフルアクセスでなくともよいが、とりあえず

*3:これはCodeBuildと組み合わせればゴリ押しで解決できる気がするのであまり気にしていない。

【AWS Athena】Lambdaからクエリを実行した際のIAM由来のエラーと対策

Athenaをテストした際に遭遇したエラーを以下にまとめる。別のエラーに遭遇した場合は後日追加していく。

Unable to Verify/Create Output Bucket

Unable to Verify/Create Output Bucket

S3へのアクセス権の不足が原因。以下のポリシーがアタッチされているか確認する。

  • s3:ListBucket
  • s3:ListBucketMultipartUploads
  • s3:GetObject
  • s3:GetBucketLocation
  • s3:ListMultipartUploadParts
  • s3:AbortMultipartUpload
  • s3:PutObject

Access Denied

Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; 

S3へのアクセス権の不足。 以下のポリシーがアタッチされているか確認する。

  • s3:PutObject
  • s3:ListObject

Insufficient permissions to execute the query.

Insufficient permissions to execute the query. User: ROLE_NAME is not authorized to perform: glue:GetTable on resource: arn:aws:glue:ap-northeast-1:UserId:catalog

エラーコードの通り、glue:GetTableのアクセス権が不足している。

HIVE_CANNOT_OPEN_SPLIT

Your query has the following errors:HIVE_CANNOT_OPEN_SPLIT: Error opening Hive split s3://BUCKET_NAME/part-00000-feda4fa5-d862-48ba-88ad-3a468e234a27-c000.snappy.parquet (offset=0, length=7791): com.amazonaws.services.s3.model.AmazonS3Exception: Forbidden (Service: Amazon S3; Status Code:403; Error Code: 403 Forbidden; Request ID: 028A0DA47697C2D7;(以下略)

この記事の通り、glue:GetTablesのアクセス権が不足している。

特に、自分でポリシーを作成した際に忘れやすい。

【AWS Athena】str型Timestampをクエリする

課題

以下のようなデータがあったとする。

{
  "timestamp": "2019-02-01 00:00:00",
  "data": 100
}
{
  "timestamp": "2019-03-01 00:00:00",
  "data": 100
}
{
  "timestamp": "2019-04-01 00:00:00",
  "data": 100
}

このデータを年月ごとに集計するクエリを作成したい。。 正直に以下のSQLでクエリすると次のようになる

SELECT year(timestamp) as year,
            month(timestamp) as month,
            sum(data)
FROM <your_year(timestamp),month(timestamp)database>.<your_table> 
GROUP BY  year(timestamp),month(timestamp);
SYNTAX_ERROR: line 1:8: Unexpected parameters (varchar) for function year. Expected: year(timestamp) , year(timestamp with time zone) , year(date) , year(interval year to month) 

原因と対策

これは、str型のタイムスタンプがタイムスタンプとして認識されていない事が原因である。 したがって以下のように記述する必要がある。

SELECT year(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),
            month(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),
            sum(data)
FROM <your_year(timestamp),month(timestamp)database>.<your_table> 
GROUP BY  year(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),month(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\'));

参考

AWS AthenaはPrestoベースであることがわかったため、以下のサイトを参考にした * Prestoでの日付の扱い方 - ★データ解析備忘録★ * prestoの気持ち 時間関係(Date and Time Functions and Operators) - Qiita

【AWS Athena】HIVE_CANNOT_OPEN_SPLITが発生する

課題

Glueでparquet.snappyにしたファイルを、Athenaで検索している。 これを定期実行するために、Lambdaを使いたい。 しかし、LambdaからAthenaにクエリを投げると、以下のエラーが発生する。

Your query has the following errors:HIVE_CANNOT_OPEN_SPLIT: Error opening Hive split s3://BUCKET_NAME/part-00000-feda4fa5-d862-48ba-88ad-3a468e234a27-c000.snappy.parquet (offset=0, length=7791): com.amazonaws.services.s3.model.AmazonS3Exception: Forbidden (Service: Amazon S3; Status Code:403; Error Code: 403 Forbidden; Request ID: 028A0DA47697C2D7;(以下略)

これがエラーコードで検索をかけても引っかからない。

気がかりな点は、rootで実行するとこのエラーは発生しない点だ。つまり、IAMポリシーが原因である可能性が高い。 なお、このエラーが最初に発生した際のIAMポリシーは以下のように設定していた。

Policies:
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - s3:PutObject
                - s3:GetBucketLocation
                - s3:ListBucket
                - s3:ListBucketMultipartUploads
                - s3:ListMultipartUploadParts
                - s3:AbortMultipartUpload
              Resource: "*"
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: "*"
            - Effect: Allow
              Action:
                - "athena:GetWorkGroup"
                - "athena:StartQueryExecution"
                - "athena:GetQueryResultsStream"
                - "athena:CancelQueryExecution"
                - "athena:StopQueryExecution"
                - "athena:GetQueryExecution"
                - "athena:GetQueryResults"
                - "athena:BatchGetNamedQuery"
                - "athena:GetNamedQuery"
                - "athena:ListTagsForResource"
                - "athena:BatchGetQueryExecution"
              Resource: "*"
            - Effect: Allow
              Action:
                - glue:GetTable
                - glue:GetDatabase
                - glue:GetPartition
                - glue:GetPartitions
                - glue:BatchGetPartition
              Resource: "*"

結論

試行錯誤した結果、glue:GetTablesのポリシーが付与されていないと、このエラーが発生するようだ。

【CloudFormation】Error: The policy failed legacy parsing

やりたい事

以下のページを参考に、別アカウントのCodePipelineに、CodeCommitの変更を配信したい。また、これをCloudFormationを使ってできる限り省力化したい。

dev.classmethod.jp

発生したエラー

開発アカウント側で、IAM Policyを作成する際にエラーが発生した。以下がテンプレートである。

AllowTargetAccessPolicy:
    Type: AWS::IAM::Policy
    Properties: 
      Roles: 
        - !Ref AllowTargetAccessRole
      PolicyName: 
        !Join
          - ""
          - - !Ref AWS::Region
            - !Ref AWS::AccountId
            - To
            - !Ref TargetAccountID
            - CodeCommitAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement: 
          - 
            Effect: Allow
            Action: 
              - s3:PutObject
              - s3:PutObjectAcl
            
            Resource: !Sub arn:aws:s3:::artifact-bucket-for-${TargetAccountID}-from-${AWS::AccountId}
      
          - 
            Effect: Allow
            Action: 
              - kms:DescribeKey
              - "kms:GenerateDataKey*"
              - kms:Encrypt
              - "kms:ReEncrypt*"
              - kms:Decrypt
            
            Resource: !Sub "arn:aws:kms:${AWS::Region}:${TargetAccountID}:key/${KMS_ID}

          - 
            Effect: Allow
            Action: 
              - codecommit:GetBranch
              - codecommit:GetCommit
              - codecommit:UploadArchive
              - codecommit:GetUploadArchiveStatus
              - codecommit:CancelUploadArchive
            
            Resource:
              !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepositoryName}

すると、以下のエラーが発生した。

The policy failed legacy parsing (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument;)

リソースをコメントアウトして、問題がKMSに関するポリシードキュメントブロックにあることがわかった。

解決方法

なぜか、!Sub の代わりに!Joinを使用することでエラーが回避できた。 (参考) tycoh.hatenablog.com

AllowTargetAccessPolicy:
    Type: AWS::IAM::Policy
    Properties: 
      Roles: 
        - !Ref AllowTargetAccessRole
      PolicyName: 
        !Join
          - ""
          - - !Ref AWS::Region
            - !Ref AWS::AccountId
            - To
            - !Ref TargetAccountID
            - CodeCommitAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement: 
          - 
            Effect: Allow
            Action: 
              - s3:PutObject
              - s3:PutObjectAcl
            
            Resource: !Sub arn:aws:s3:::artifact-bucket-for-${TargetAccountID}-from-${AWS::AccountId}
      
          - 
            Effect: Allow
            Action: 
              - kms:DescribeKey
              - "kms:GenerateDataKey*"
              - kms:Encrypt
              - "kms:ReEncrypt*"
              - kms:Decrypt
            
            Resource: |
              !Join
                - ""
                - - arn:aws:kms
                  - !Ref AWS::Region
                  - ":"
                  - !Ref TargetAccountID
                  - "key/"
                  - !Ref KMSArn

          - 
            Effect: Allow
            Action: 
              - codecommit:GetBranch
              - codecommit:GetCommit
              - codecommit:UploadArchive
              - codecommit:GetUploadArchiveStatus
              - codecommit:CancelUploadArchive
            
            Resource:
              !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepositoryName}

原因は後日AWS Forumに質問してみることにする。

【Error】CloudFormation:: Parameter is non alphaNumeric

現象

以下のCloudFormationテンプレートのパラメーターブロックでParameter is non alphaNumericというエラーが発生した。

---
Parameter:
  MY_MESSAGE: This is test
---

原因

パラメーターブロックでは、記号を使うことができない。そのため、アンダーバーを使うことができない。 そのため、キャメルケースでパラメーターを設定する。

Parameter:
  myMessage: This is test
  # または、MyMessage

参考

CloudFormation Parameter Template Error : Parameter is non alphanumeric