From 81b7394b478304976b1433d0a9e8298eb012ac82 Mon Sep 17 00:00:00 2001 From: Joni Harker <506966+ConsoleCatzirl@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:20:33 -0700 Subject: [PATCH] [IT-3068] Lambda for emailing monthly S3 cost reports (#1) * Fork from Sage-Bionetworks-IT/lambda-finops-email-totals#1.1.4 * Rename to lamda-finops-s3-cost-report * Convert lambda to meet STRIDES' needs for IT-3068 Send a monthly email with the total monthly costs broken down by AWS service, and the total monthly S3 costs broken down by usage type. --- .coveragerc | 2 +- .../action.yaml | 12 +- .github/workflows/post-merge.yaml | 9 +- .github/workflows/test.yaml | 13 +- .gitignore | 1 + .pre-commit-config.yaml | 13 +- Pipfile | 11 +- Pipfile.lock | 1574 +++++++++++++++-- README.md | 252 ++- hello_world/app.py | 42 - {hello_world => s3_cost_report}/__init__.py | 0 s3_cost_report/app.py | 197 +++ s3_cost_report/ce.py | 70 + s3_cost_report/ses.py | 264 +++ template.yaml | 125 +- tests/unit/conftest.py | 172 ++ tests/unit/test_app.py | 129 ++ tests/unit/test_ce.py | 26 + tests/unit/test_handler.py | 73 - tests/unit/test_ses.py | 38 + 20 files changed, 2692 insertions(+), 331 deletions(-) rename .github/actions/{sam-build => sam-build-and-lint}/action.yaml (73%) delete mode 100644 hello_world/app.py rename {hello_world => s3_cost_report}/__init__.py (100%) create mode 100644 s3_cost_report/app.py create mode 100644 s3_cost_report/ce.py create mode 100644 s3_cost_report/ses.py create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_app.py create mode 100644 tests/unit/test_ce.py delete mode 100644 tests/unit/test_handler.py create mode 100644 tests/unit/test_ses.py diff --git a/.coveragerc b/.coveragerc index e0df967..f2af0b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,4 @@ relative_files = True # Use 'source' instead of 'omit' in order to ignore 'tests/unit/__init__.py' -source = hello_world +source = s3_cost_report diff --git a/.github/actions/sam-build/action.yaml b/.github/actions/sam-build-and-lint/action.yaml similarity index 73% rename from .github/actions/sam-build/action.yaml rename to .github/actions/sam-build-and-lint/action.yaml index 317befd..d97ccd9 100644 --- a/.github/actions/sam-build/action.yaml +++ b/.github/actions/sam-build-and-lint/action.yaml @@ -1,4 +1,4 @@ -name: sam-build +name: sam-build-and-lint runs: # This creates a composite action to be used as a step in a job @@ -8,7 +8,7 @@ runs: # Convert Pipfile.lock to requirements.txt for sam - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - run: pip install -U pipenv shell: bash @@ -21,6 +21,14 @@ runs: with: use-installer: true + # Lint the input template + - run: sam validate --lint + shell: bash + # Use a lambda-like docker container to build the lambda artifact - run: sam build --use-container shell: bash + + # Lint the built template + - run: sam validate --lint --template .aws-sam/build/template.yaml + shell: bash diff --git a/.github/workflows/post-merge.yaml b/.github/workflows/post-merge.yaml index 1459801..d7fc121 100644 --- a/.github/workflows/post-merge.yaml +++ b/.github/workflows/post-merge.yaml @@ -24,8 +24,8 @@ jobs: steps: - uses: actions/checkout@v4 - # Install sam-cli and run "sam build" - - uses: ./.github/actions/sam-build + # install sam-cli, build, and lint + - uses: ./.github/actions/sam-build-and-lint # authenticate with AWS via OIDC - uses: aws-actions/configure-aws-credentials@v4 @@ -38,5 +38,8 @@ jobs: # upload the lambda artifact to s3 and generate a cloudformation template referencing it - run: sam package --template-file .aws-sam/build/template.yaml --s3-bucket $ESSENTIALS_BUCKET --s3-prefix ${{ github.event.repository.name }}/${{ github.ref_name }} --output-template-file .aws-sam/build/${{ github.event.repository.name }}.yaml - # upload the generated cloudformation template to s3 + # validate final template with cloudformation + - run: aws cloudformation validate-template --template-body file://.aws-sam/build/${{ github.event.repository.name }}.yaml + + # upload the final template to s3 - run: aws s3 cp .aws-sam/build/${{ github.event.repository.name }}.yaml s3://$BOOTSTRAP_BUCKET/${{ github.event.repository.name }}/${{ github.ref_name }}/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dbfe016..d62c471 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - uses: pre-commit/action@v3.0.1 pytest: @@ -23,18 +23,19 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - run: pip install -U pipenv - - run: pipenv install --dev - - run: pipenv run coverage run -m pytest tests/ -vv + - run: pipenv sync --dev + - run: pipenv run coverage run -m pytest tests/ -svv + - run: pipenv run coverage lcov - name: upload coverage to coveralls uses: coverallsapp/github-action@v2 with: + file: coverage.lcov fail-on-error: false sam-build-and-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/sam-build - - run: sam validate --lint --template .aws-sam/build/template.yaml + - uses: ./.github/actions/sam-build-and-lint diff --git a/.gitignore b/.gitignore index 2ca8ac8..6d8b05d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +coverage.lcov # Translations *.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fb5726..92fbe11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,12 @@ repos: rev: v1.5.5 hooks: - id: remove-tabs -- repo: https://github.com/aristanetworks/j2lint.git - rev: v1.1.0 - hooks: - - id: j2lint +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.18 # Use the ref you want to point at + hooks: + - id: mdformat + # Optionally add plugins + additional_dependencies: + - mdformat-gfm # github-flavored markdown + files: README.md + args: [--wrap=80] diff --git a/Pipfile b/Pipfile index d11ca0c..a34e402 100644 --- a/Pipfile +++ b/Pipfile @@ -4,13 +4,14 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -pytest = "~=7.1" +pytest = "~=8.3" pytest-mock = "~=3.8" -boto3 = "~=1.24" -coverage = "~=7.3" +coverage = "~=7.0" +pre-commit = "~=4.0" +aws-sam-cli = "~=1.0" [packages] -crhelper = "~=2.0" +boto3 = "~=1.24" [requires] -python_version = "3.9" +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index 88fab3a..6896bd7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "28f91f2ef755c20ea49979be1052af8b372c53fbef9926e51ca1bb350ad76256" + "sha256": "910fbbcdb8d09badee0e05b2b4576cda56428bd159ff5d80d331a2312f2ef4be" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "3.12" }, "sources": [ { @@ -16,99 +16,573 @@ ] }, "default": { - "crhelper": { + "boto3": { "hashes": [ - "sha256:0c1f703a830722379d205d58ca4f0da768c0b10670ddce46af31ba9661bf2d5a", - "sha256:da9efe4fb57d86f0567fddc999ae1c242ea9602c95b165b09e00d435c3845ef0" + "sha256:33c6a7aeab316f7e0b3ad8552afe95a4a10bfd58519d00741c4d4f3047da8382", + "sha256:9352f6d61f15c789231a5d608613f03425059072ed862c32e1ed102b17206abf" ], "index": "pypi", - "version": "==2.0.11" + "markers": "python_version >= '3.8'", + "version": "==1.35.40" + }, + "botocore": { + "hashes": [ + "sha256:072cc47f29cb1de4fa77ce6632e4f0480af29b70816973ff415fbaa3f50bd1db", + "sha256:547e0a983856c7d7aeaa30fca2a283873c57c07366cd806d2d639856341b3c31" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.40" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "s3transfer": { + "hashes": [ + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.3" } }, "develop": { + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, + "arrow": { + "hashes": [ + "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", + "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + ], + "markers": "python_version >= '3.7'", + "version": "==24.2.0" + }, + "aws-lambda-builders": { + "hashes": [ + "sha256:40a613ecb19fbf0b64a47bae14bd252ea5da32ea71fde9808d596e2dbc011baf", + "sha256:ad95ed55359c399872f5825582896500dfc1c5564eccf2a6ab8d0e9f6c1ae385" + ], + "markers": "python_version >= '3.8'", + "version": "==1.50.0" + }, + "aws-sam-cli": { + "hashes": [ + "sha256:3bfbabc1471e161b738e29fec569c1678a172e08ae610adf7b57819ada18c6de", + "sha256:de6569d02a989610c79ac28101c801d2db5738cce2b1d4bffaffbc0c0d191e8e" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_version != '4.0' and python_version <= '4.0'", + "version": "==1.125.0" + }, + "aws-sam-translator": { + "hashes": [ + "sha256:0cdfbc598f384c430c3ec064f6008d80c5a0d58f1dc45ca4e331ae5c43cb4697", + "sha256:9ebf4b53c226338e6b89d14d8583bc4559b87f0be52ed8d577c5a1dc2db14962" + ], + "markers": "python_version >= '3.8' and python_version != '4.0' and python_version <= '4.0'", + "version": "==1.91.0" + }, + "binaryornot": { + "hashes": [ + "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", + "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4" + ], + "version": "==0.4.4" + }, + "blinker": { + "hashes": [ + "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", + "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" + ], + "markers": "python_version >= '3.8'", + "version": "==1.8.2" + }, "boto3": { "hashes": [ - "sha256:2680c0e36167e672777110ccef5303d59fa4a6a4f10086f9c14158c5cb008d5c", - "sha256:2ceb644b1df7c3c8907913ab367a9900af79e271b4cfca37b542ec1fa142faf8" + "sha256:33c6a7aeab316f7e0b3ad8552afe95a4a10bfd58519d00741c4d4f3047da8382", + "sha256:9352f6d61f15c789231a5d608613f03425059072ed862c32e1ed102b17206abf" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.28.55" + "markers": "python_version >= '3.8'", + "version": "==1.35.40" + }, + "boto3-stubs": { + "extras": [ + "apigateway", + "cloudformation", + "ecr", + "iam", + "kinesis", + "lambda", + "s3", + "schemas", + "secretsmanager", + "signer", + "sqs", + "stepfunctions", + "sts", + "xray" + ], + "hashes": [ + "sha256:15e0825f0338dfd6f8623ecce44bd2369aabae7aeea3f706772c9172b6f3cff6", + "sha256:78d96e7135e2ed2743211000b7a592c90ea1caf6c2a203f4778af527da7353c1" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.23" }, "botocore": { "hashes": [ - "sha256:159f637300206a0b37b49c1bee61265650843f591e9cb62e9adcb3d1c2afec91", - "sha256:6485a700744c60fcbf4bba4fcacb22067f601e79fb0c27fae04cf07b03c5e8f9" + "sha256:072cc47f29cb1de4fa77ce6632e4f0480af29b70816973ff415fbaa3f50bd1db", + "sha256:547e0a983856c7d7aeaa30fca2a283873c57c07366cd806d2d639856341b3c31" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.40" + }, + "botocore-stubs": { + "hashes": [ + "sha256:266e72603cad6927040b2a2d7dcbf0b11240833c213274a0bec8052b214931c0", + "sha256:a1eb2fb186059d667f0583df6d4d3419b7e47c33ad9ab082e970f819c0f511af" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.40" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, + "cfgv": { + "hashes": [ + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "cfn-lint": { + "hashes": [ + "sha256:34343ec06fc9a08d8947fa1cd61d1e8ad3430ee745654099752da6f7d72f880d", + "sha256:e88c473684fe78a23b6ccdce2fd393cf6a52a3a4b2b6e7160888bd41bd07aa8c" + ], + "markers": "python_version >= '3.8'", + "version": "==1.14.2" + }, + "chardet": { + "hashes": [ + "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", + "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970" + ], + "markers": "python_version >= '3.7'", + "version": "==5.2.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.4.0" + }, + "chevron": { + "hashes": [ + "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", + "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443" + ], + "version": "==0.14.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "cookiecutter": { + "hashes": [ + "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", + "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c" ], "markers": "python_version >= '3.7'", - "version": "==1.31.59" + "version": "==2.6.0" }, "coverage": { "hashes": [ - "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", - "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", - "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", - "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", - "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", - "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", - "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", - "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", - "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", - "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", - "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", - "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", - "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", - "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", - "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", - "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", - "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", - "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", - "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", - "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", - "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", - "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", - "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", - "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", - "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", - "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", - "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", - "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", - "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", - "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", - "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", - "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", - "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", - "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", - "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", - "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", - "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", - "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", - "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", - "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", - "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", - "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", - "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", - "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", - "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", - "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", - "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", - "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", - "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", - "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", - "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", - "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" + "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6", + "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2", + "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba", + "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb", + "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6", + "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4", + "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0", + "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6", + "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990", + "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3", + "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43", + "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175", + "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a", + "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6", + "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97", + "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b", + "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e", + "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39", + "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd", + "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d", + "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f", + "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc", + "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976", + "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549", + "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c", + "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5", + "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4", + "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b", + "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e", + "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3", + "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6", + "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e", + "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929", + "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234", + "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13", + "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007", + "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3", + "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167", + "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d", + "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d", + "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40", + "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181", + "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054", + "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd", + "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2", + "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91", + "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3", + "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b", + "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38", + "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd", + "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f", + "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2", + "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba", + "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f", + "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83", + "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce", + "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38", + "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c", + "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f", + "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21", + "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4", + "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92" ], "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==7.6.3" + }, + "cryptography": { + "hashes": [ + "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", + "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", + "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", + "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", + "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", + "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", + "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", + "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", + "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", + "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", + "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", + "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", + "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", + "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", + "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", + "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", + "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", + "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", + "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", + "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", + "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", + "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", + "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", + "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", + "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", + "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", + "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" + ], + "markers": "python_version >= '3.7'", + "version": "==43.0.1" + }, + "dateparser": { + "hashes": [ + "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830", + "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "distlib": { + "hashes": [ + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" + ], + "version": "==0.3.9" + }, + "docker": { + "hashes": [ + "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", + "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0" + ], "markers": "python_version >= '3.8'", - "version": "==7.3.1" + "version": "==7.1.0" }, - "exceptiongroup": { + "filelock": { "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], - "markers": "python_version < '3.11'", - "version": "==1.1.3" + "markers": "python_version >= '3.8'", + "version": "==3.16.1" + }, + "flask": { + "hashes": [ + "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", + "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.3" + }, + "identify": { + "hashes": [ + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" + ], + "markers": "python_version >= '3.8'", + "version": "==2.6.1" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "iniconfig": { "hashes": [ @@ -118,6 +592,22 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, "jmespath": { "hashes": [ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", @@ -126,55 +616,814 @@ "markers": "python_version >= '3.7'", "version": "==1.0.1" }, - "packaging": { + "jsonpatch": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", + "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.33" + }, + "jsonpointer": { + "hashes": [ + "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", + "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==3.0.0" }, - "pluggy": { + "jsonschema": { "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" ], "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" + ], + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "markupsafe": { + "hashes": [ + "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", + "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", + "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", + "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8", + "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b", + "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad", + "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a", + "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", + "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", + "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6", + "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", + "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", + "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a", + "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8", + "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", + "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", + "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170", + "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132", + "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", + "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", + "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9", + "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", + "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", + "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589", + "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", + "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915", + "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", + "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", + "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea", + "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", + "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", + "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b", + "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", + "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b", + "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7", + "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf", + "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", + "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91", + "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", + "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50", + "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b", + "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583", + "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", + "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", + "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", + "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", + "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25", + "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", + "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", + "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3", + "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97", + "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1", + "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd", + "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", + "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a", + "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", + "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca", + "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6", + "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635", + "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", + "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.1" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "mpmath": { + "hashes": [ + "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", + "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" + ], "version": "==1.3.0" }, - "pytest": { + "mypy-boto3-apigateway": { "hashes": [ - "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", - "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" + "sha256:436cee5bc8a4e75a116653dddf277b7582f331d7af8ac8c0195d2eb7828e45b8", + "sha256:ea3b419ae868d63f0613eeacc6a75861fd57b689d6a14bb7562eed303913a5ae" + ], + "version": "==1.35.25" + }, + "mypy-boto3-cloudformation": { + "hashes": [ + "sha256:0d037d9d6bdb439a84e2391ba987a4e03fcedfad0e881db1cf0f7861d275907c", + "sha256:5da07e14a206a7f0015434d1730a6a68a33167ea6746343189dd1742cfcfdb7d" + ], + "version": "==1.35.0" + }, + "mypy-boto3-ecr": { + "hashes": [ + "sha256:90a067cc131129e010887b8da3caa8d34d32da9c17121b5dc047f738c2c1f00c", + "sha256:d7e8c24086ce3b259e4ac2a271fc34ed4effde99c7899ac7d64c2da3cff0acd7" + ], + "version": "==1.35.21" + }, + "mypy-boto3-iam": { + "hashes": [ + "sha256:aaa7608799500e2a2ee241d8c3c123f6d1c2ef2d29025c5dff3ac2720a555ccc", + "sha256:b379a01c3ca17a367cb7a460905f9ce1ab7830a9abb8c8a56f28a5ff1087657f" + ], + "version": "==1.35.0" + }, + "mypy-boto3-kinesis": { + "hashes": [ + "sha256:865f2697f62dfc7d04052436a925bdf0d39a9317cde8c6981a6ac46e659c1c7f", + "sha256:f5e526f3b91ac9709c487a4334853149c2cabb41700ef93827daeb742e05c05e" + ], + "version": "==1.35.26" + }, + "mypy-boto3-lambda": { + "hashes": [ + "sha256:8473d71ee83aca8009d317e57cd2094a355ec90c7c536cf26e52db71b2f7528b", + "sha256:e42d9ce7e6a32841e4a6a2980f5f8634e2b0a35698e71d302a78e4d0de4223c6" + ], + "version": "==1.35.28" + }, + "mypy-boto3-s3": { + "hashes": [ + "sha256:ad77a637c71295a693c616d0dcfadca9ee0a5426083fe40a4eb19e84453dd9fa", + "sha256:fceeb10ea70991a516b34d11c1fde0f7f3fe7508df4e436ffe066d27d04db0e4" + ], + "version": "==1.35.32" + }, + "mypy-boto3-schemas": { + "hashes": [ + "sha256:a63cbf1c5189e29638b7f1522357db080bfd99e142110a06df6192b5d68f0dc8", + "sha256:bd138d1a8ab1f075bf222399d5bff107c7cd77e06560ab5dc2fed21392212919" + ], + "version": "==1.35.0" + }, + "mypy-boto3-secretsmanager": { + "hashes": [ + "sha256:c37d181315ba10d8546872304d7f266e7461429b08e63507c23cc508c3ef4264", + "sha256:ff72d5743061d1d9bf3f5e308990b78c9bede8e02648f6eb8712e3b2e76d2669" + ], + "version": "==1.35.0" + }, + "mypy-boto3-signer": { + "hashes": [ + "sha256:06653bbc2b92f0ec390d2622e2a6cbad8a191ac758c21e0803d1151cd18c8232", + "sha256:4138a5105ccb321530ae2c7a1ec1d4f7efa354bd87ac10f965d560688a7f2084" + ], + "version": "==1.35.0" + }, + "mypy-boto3-sqs": { + "hashes": [ + "sha256:61752f1c2bf2efa3815f64d43c25b4a39dbdbd9e472ae48aa18d7c6d2a7a6eb8", + "sha256:9fd6e622ed231c06f7542ba6f8f0eea92046cace24defa95d0d0ce04e7caee0c" + ], + "version": "==1.35.0" + }, + "mypy-boto3-stepfunctions": { + "hashes": [ + "sha256:c088ab67751e837c5db40593d6cbb70505c8e56e0e04fb32a0241e79bb22812b", + "sha256:c0da4f9cd7bd2cf981fcee0bd8c5509d5841f27303771450d656ebf8eac4c977" + ], + "version": "==1.35.9" + }, + "mypy-boto3-sts": { + "hashes": [ + "sha256:50c6cd996dcff91d58295b6afd4e27201d1e4bfc75d0190eadee052f105bc602", + "sha256:619580c0bcf4d7f79808c8328a7894a0eeac56f94541833c5a329cbc708f7678" + ], + "version": "==1.35.0" + }, + "mypy-boto3-xray": { + "hashes": [ + "sha256:a3c3a6d83f659f6dc4dbf392ac1481029af6b941e9485ea4878bbf60e338f82c", + "sha256:c3c7aff1b2d05e218f991ab74101d2296927553bbb7d4b2d961ffb7326995931" + ], + "version": "==1.35.0" + }, + "networkx": { + "hashes": [ + "sha256:e30a87b48c9a6a7cc220e732bffefaee585bdb166d13377734446ce1a0620eed", + "sha256:f9df45e85b78f5bd010993e897b4f1fdb242c11e015b101bd951e5c0e29982d8" + ], + "markers": "python_version >= '3.10'", + "version": "==3.4.1" + }, + "nodeenv": { + "hashes": [ + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.9.1" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "platformdirs": { + "hashes": [ + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.6" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pre-commit": { + "hashes": [ + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.0.1" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pydantic": { + "hashes": [ + "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", + "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12" + ], + "markers": "python_version >= '3.8'", + "version": "==2.9.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", + "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", + "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", + "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", + "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", + "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", + "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", + "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", + "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", + "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", + "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", + "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", + "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", + "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", + "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", + "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", + "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368", + "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", + "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", + "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2", + "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", + "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", + "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", + "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", + "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", + "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", + "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271", + "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", + "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb", + "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13", + "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", + "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556", + "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665", + "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", + "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", + "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", + "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", + "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", + "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", + "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", + "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", + "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", + "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", + "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", + "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", + "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", + "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658", + "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", + "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", + "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", + "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", + "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", + "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", + "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", + "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", + "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", + "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", + "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad", + "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", + "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", + "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", + "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", + "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", + "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", + "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", + "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", + "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", + "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", + "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555", + "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", + "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6", + "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", + "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", + "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", + "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", + "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", + "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", + "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", + "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", + "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12", + "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", + "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", + "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", + "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", + "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", + "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", + "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", + "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", + "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607" + ], + "markers": "python_version >= '3.8'", + "version": "==2.23.4" + }, + "pygments": { + "hashes": [ + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, + "pyopenssl": { + "hashes": [ + "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", + "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d" + ], "markers": "python_version >= '3.7'", - "version": "==7.4.2" + "version": "==24.2.1" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.3" }, "pytest-mock": { "hashes": [ - "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39", - "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f" + "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", + "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.11.1" + "markers": "python_version >= '3.8'", + "version": "==3.14.0" }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, - "s3transfer": { + "python-slugify": { "hashes": [ - "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", - "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" + "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", + "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856" ], "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "version": "==8.0.4" + }, + "pytz": { + "hashes": [ + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" + ], + "version": "==2024.2" + }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" + }, + "regex": { + "hashes": [ + "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", + "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", + "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", + "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", + "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", + "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", + "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", + "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", + "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", + "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", + "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", + "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", + "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", + "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", + "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", + "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", + "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", + "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", + "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", + "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", + "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", + "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", + "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", + "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", + "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", + "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", + "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", + "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", + "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", + "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", + "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", + "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", + "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", + "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", + "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", + "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", + "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", + "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", + "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", + "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", + "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", + "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", + "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", + "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", + "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", + "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", + "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", + "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", + "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", + "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", + "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", + "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", + "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", + "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", + "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", + "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", + "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", + "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", + "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", + "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", + "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", + "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", + "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", + "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", + "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", + "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", + "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", + "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", + "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", + "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", + "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", + "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", + "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", + "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", + "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", + "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", + "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", + "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", + "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", + "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", + "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", + "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", + "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", + "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", + "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", + "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", + "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", + "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", + "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", + "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", + "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", + "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", + "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + ], + "markers": "python_version >= '3.8'", + "version": "==2024.9.11" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "rich": { + "hashes": [ + "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", + "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==13.8.1" + }, + "rpds-py": { + "hashes": [ + "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", + "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", + "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5", + "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6", + "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", + "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", + "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29", + "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318", + "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b", + "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399", + "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", + "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee", + "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", + "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a", + "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", + "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", + "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03", + "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5", + "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22", + "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e", + "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", + "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", + "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752", + "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", + "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253", + "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", + "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad", + "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5", + "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", + "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7", + "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", + "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", + "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", + "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", + "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec", + "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209", + "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921", + "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", + "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074", + "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580", + "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", + "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5", + "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3", + "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0", + "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", + "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", + "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db", + "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", + "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789", + "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", + "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", + "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c", + "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232", + "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", + "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c", + "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", + "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489", + "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", + "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751", + "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", + "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda", + "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", + "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", + "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", + "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8", + "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989", + "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", + "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1", + "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2", + "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", + "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c", + "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965", + "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", + "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58", + "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b", + "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", + "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", + "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", + "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de", + "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", + "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", + "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272", + "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", + "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", + "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1", + "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", + "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", + "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", + "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364", + "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", + "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", + "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420", + "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5", + "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24", + "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c", + "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", + "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f", + "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e", + "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab", + "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08", + "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", + "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", + "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "ruamel-yaml": { + "hashes": [ + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" + ], + "markers": "python_version >= '3.7'", + "version": "==0.18.6" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", + "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", + "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", + "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", + "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", + "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", + "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", + "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", + "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", + "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", + "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", + "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", + "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", + "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", + "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", + "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", + "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", + "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", + "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", + "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", + "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", + "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", + "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", + "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", + "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", + "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", + "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", + "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", + "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", + "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", + "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", + "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", + "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", + "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", + "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", + "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", + "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", + "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", + "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", + "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", + "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", + "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", + "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", + "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", + "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", + "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", + "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", + "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", + "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", + "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" + ], + "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", + "version": "==0.2.8" + }, + "s3transfer": { + "hashes": [ + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, + "setuptools": { + "hashes": [ + "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", + "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" + ], + "markers": "python_version >= '3.8'", + "version": "==75.1.0" }, "six": { "hashes": [ @@ -184,22 +1433,141 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "tomli": { + "sympy": { + "hashes": [ + "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", + "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9" + ], + "markers": "python_version >= '3.8'", + "version": "==1.13.3" + }, + "text-unidecode": { + "hashes": [ + "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", + "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" + ], + "version": "==1.3" + }, + "tomlkit": { + "hashes": [ + "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", + "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" + ], + "markers": "python_version >= '3.8'", + "version": "==0.13.2" + }, + "types-awscrt": { + "hashes": [ + "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54", + "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9" + ], + "markers": "python_version >= '3.8'", + "version": "==0.22.0" + }, + "types-python-dateutil": { + "hashes": [ + "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d", + "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446" + ], + "markers": "python_version >= '3.8'", + "version": "==2.9.0.20241003" + }, + "types-s3transfer": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:d34c5a82f531af95bb550927136ff5b737a1ed3087f90a59d545591dfde5b4cc", + "sha256:f761b2876ac4c208e6c6b75cdf5f6939009768be9950c545b11b0225e7703ee7" ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "tzlocal": { + "hashes": [ + "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", + "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e" + ], + "markers": "python_version >= '3.8'", + "version": "==5.2" }, "urllib3": { "hashes": [ - "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21", - "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.17" + "markers": "python_version >= '3.10'", + "version": "==2.2.3" + }, + "virtualenv": { + "hashes": [ + "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", + "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2" + ], + "markers": "python_version >= '3.7'", + "version": "==20.26.6" + }, + "watchdog": { + "hashes": [ + "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", + "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", + "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", + "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", + "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", + "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", + "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", + "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", + "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", + "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", + "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", + "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", + "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", + "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", + "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", + "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", + "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", + "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", + "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", + "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", + "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", + "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", + "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", + "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", + "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", + "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", + "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", + "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", + "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", + "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", + "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", + "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", + "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", + "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", + "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.2" + }, + "werkzeug": { + "hashes": [ + "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", + "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.4" + }, + "wheel": { + "hashes": [ + "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f", + "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49" + ], + "markers": "python_version >= '3.8'", + "version": "==0.44.0" } } } diff --git a/README.md b/README.md index d02edfa..7b1cf7d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,80 @@ -# lambda-template -A GitHub template for quickly starting a new AWS lambda project. +# lambda-finops-s3-cost-report -## Naming -Naming conventions: -* for a vanilla Lambda: `lambda-` -* for a Cloudformation Transform macro: `cfn-macro-` -* for a Cloudformation Custom Resource: `cfn-cr-` +An AWS Lambda for emailing monthly S3 service totals for the current account. + +## Design + +This lambda will query Cost Explorer for monthly totals grouped by service, and +also S3-specific totals grouped by S3 usage type (e.g. bytes transferred), then +send an email report of the results to the given recipients. + +### Parameters + +| Parameter Name | Allowed Values | Default Value | Description | +| ------------------ | --------------------------------------- | --------------------- | -------------------------------------------- | +| Sender | SES verified identity | Required Value | Value to use for the `From` email
field | +| Recipients | Comma-delimited list of email addresses | Required Value | The list of email recipients | +| OmitCostsLessThan | Floating-point number | `0.01` | Totals less than this amount will be ignored | +| ScheduleExpression | EventBridge Schedule Expression | `cron(30 10 2 * ? *)` | Schedule for running the lambda | + +#### Sender + +This email address will appear is the `From` field, and must be +[verified](https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html) +before emails will successfully send. + +#### Recipients + +The list of email recipients for email reports. + +#### OmitCostsLessThan + +Don't include totals less than this amount in the report. + +#### ScheduleExpression + +[EventBridge schedule expression](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents-expressions.html) +describing how often to run the lambda. By default it runs at 10:30am UTC on the +2nd of each month. + +### Triggering + +The lambda is configured to run on a schedule, by default at 10:30am UTC on the +2nd of each month. Ad-hoc runs for testing can be triggered with an empty test +event from the +[Lambda console page](https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html) ## Development ### Contributions + Contributions are welcome. ### Setup Development Environment Install the following applications: -* [AWS CLI](https://github.com/aws/aws-cli) -* [AWS SAM CLI](https://github.com/aws/aws-sam-cli) -* [pre-commit](https://github.com/pre-commit/pre-commit) -* [pipenv](https://github.com/pypa/pipenv) + +- [AWS CLI](https://github.com/aws/aws-cli) +- [AWS SAM CLI](https://github.com/aws/aws-sam-cli) +- [pre-commit](https://github.com/pre-commit/pre-commit) +- [pipenv](https://github.com/pypa/pipenv) + +Check in the [lambda-test](.github/workflows/test.yaml) workflow to see how they +are installed for automated testing. ### Install Requirements -Run `pipenv install --dev` to install both production and development -requirements, and `pipenv shell` to activate the virtual environment. For more -information see the [pipenv docs](https://pipenv.pypa.io/en/latest/). + +Run `pipenv sync --dev` to install both production and development requirements, +and `pipenv shell` to activate the virtual environment. For more information see +the [pipenv docs](https://pipenv.pypa.io/en/latest/). After activating the virtual environment, run `pre-commit install` to install the [pre-commit](https://pre-commit.com/) git hook. ### Update Requirements -First, make any needed updates to the base requirements in `Pipfile`, -then use `pipenv` to regenerate both `Pipfile.lock` and -`requirements.txt`. We use `pipenv` to control versions in testing, -but `sam` relies on `requirements.txt` directly for building the -container used by the lambda. + +First, make any needed updates to the base requirements in `Pipfile`, then use +`pipenv` to regenerate both `Pipfile.lock` and `requirements.txt`. ```shell script $ pipenv update --dev @@ -42,45 +83,101 @@ $ pipenv update --dev We use `pipenv` to control versions in testing, but `sam` relies on `requirements.txt` directly for building the lambda artifact, so we dynamically generate `requirements.txt` from `Pipfile.lock` before building the artifact. -The file must be created in the `CodeUri` directory specified in `template.yaml`. +The file must be created in the `CodeUri` directory specified in +`template.yaml`. ```shell script $ pipenv requirements > requirements.txt ``` Additionally, `pre-commit` manages its own requirements. + ```shell script $ pre-commit autoupdate ``` ### Create a local build + Use a Lambda-like docker container to build the Lambda artifact + ```shell script $ sam build --use-container ``` ### Run unit tests + Tests are defined in the `tests` folder in this project, and dependencies are managed with `pipenv`. Install the development dependencies and run the tests using `coverage`. ```shell script -$ pipenv run coverage run -m pytest tests/ -vv +$ coverage run -m pytest tests/ -svv +``` + +And to view the coverage report: + +```shell script +$ coverage report -m ``` Automated testing will upload coverage results to [Coveralls](coveralls.io). +### Lint and validate Cloudformation templates + +## Lint input template with SAM CLI + +Lint the SAM input template (`template.yaml`) using the SAM CLI. + +```shell script +$ sam validate --lint +``` + +## Validate input template with SAM CLI + +Validate the SAM input template using the SAM CLI. Requires AWS authentication. + +```shell script +$ sam validate +``` + +## Validate SAM input template with AWS CLI + +Validate the SAM input template using the AWS CLI. Requires AWS Authentication. + +```shell script +$ aws cloudformation validate-template --template-body file://template.yaml +``` + +## Validate SAM build template with AWS CLI + +Validate the SAM build template using the AWS CLI. Requires AWS Authentication. + +```shell script +$ aws cloudformation validate-template --template-body file://.aws-sam/build/template.yaml +``` + +## Validate SAM package template with AWS CLI + +Validate the SAM package template using the AWS CLI. Requires AWS +Authentication. + +```shell script +$ aws cloudformation validate-template --template-body file://.aws-sam/build/output.yaml +``` + ### Run integration tests + Running integration tests [requires docker](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html) ```shell script -$ sam local invoke HelloWorldFunction --event events/event.json +$ sam local invoke MonthlyS3Usage --event events/event.json ``` ## Deployment ### Deploy Lambda to S3 + Deployments are sent to the [Sage cloudformation repository](https://bootstrap-awss3cloudformationbucket-19qromfd235z9.s3.amazonaws.com/index.html) which requires permissions to upload to Sage @@ -90,22 +187,25 @@ which requires permissions to upload to Sage ```shell script sam package --template-file .aws-sam/build/template.yaml \ --s3-bucket essentials-awss3lambdaartifactsbucket-x29ftznj6pqw \ - --output-template-file .aws-sam/build/lambda-template.yaml + --output-template-file .aws-sam/build/output.yaml -aws s3 cp .aws-sam/build/lambda-template.yaml s3://bootstrap-awss3cloudformationbucket-19qromfd235z9/lambda-template/master/ +aws s3 cp .aws-sam/build/output.yaml s3://bootstrap-awss3cloudformationbucket-19qromfd235z9/lambda-finops-s3-cost-report/VERSION/ ``` ## Publish Lambda ### Private access -Publishing the lambda makes it available in your AWS account. It will be accessible in -the [serverless application repository](https://console.aws.amazon.com/serverlessrepo). + +Publishing the lambda makes it available in your AWS account. It will be +accessible in the +[serverless application repository](https://console.aws.amazon.com/serverlessrepo). ```shell script -sam publish --template .aws-sam/build/lambda-template.yaml +sam publish --template .aws-sam/build/output.yaml ``` ### Public access + Making the lambda publicly accessible makes it available in the [global AWS serverless application repository](https://serverlessrepo.aws.amazon.com/applications) @@ -117,40 +217,106 @@ aws serverlessrepo put-application-policy \ ## Install Lambda into AWS +This lambda is intended to run in a stand-alone account. When using AWS +Organizations, deploying the lambda to the payer account will aggregate costs +from all member accounts. To get costs for a single member account, deploy the +lambda to that member account. + ### Sceptre + Create the following [sceptre](https://github.com/Sceptre/sceptre) file -config/prod/lambda-template.yaml +config/prod/lambda-finops-s3-cost-report.yaml ```yaml template: type: http - url: "https://PUBLISH_BUCKET.s3.amazonaws.com/lambda-template/VERSION/lambda-template.yaml" -stack_name: "lambda-template" + url: "https://PUBLISH_BUCKET.s3.amazonaws.com/lambda-finops-s3-cost-report/VERSION/lambda-finops-s3-cost-report.yaml" +stack_name: "lambda-finops-s3-cost-report" stack_tags: - Department: "Platform" - Project: "Infrastructure" OwnerEmail: "it@sagebase.org" +parameters: + Sender: "it@sagebase.org" + Recipients: "account-admin@sagebase.org" ``` Install the lambda using sceptre: + ```shell script -sceptre --var "profile=my-profile" --var "region=us-east-1" launch prod/lambda-template.yaml +sceptre --var "profile=my-profile" --var "region=us-east-1" launch prod/lambda-finops-s3-cost-report.yaml ``` ### AWS Console + Steps to deploy from AWS console. 1. Login to AWS -2. Access the -[serverless application repository](https://console.aws.amazon.com/serverlessrepo) --> Available Applications -3. Select application to install -4. Enter Application settings -5. Click Deploy +1. Access the + [serverless application repository](https://console.aws.amazon.com/serverlessrepo) + -> Available Applications +1. Select application to install +1. Enter Application settings +1. Click Deploy ## Releasing -We have setup our CI to automate a releases. To kick off the process just create -a tag (i.e 0.0.1) and push to the repo. The tag must be the same number as the current -version in [template.yaml](template.yaml). Our CI will do the work of deploying and publishing -the lambda. +We have setup our CI to automate a releases. To kick off the process just create +a tag (i.e 0.0.1) and push to the repo. The tag must be the same number as the +current version in [template.yaml](template.yaml). Our CI will do the work of +deploying and publishing the lambda. + +### Initial Deploy + +Some manual verification and testing must be performed with the initial deploy. + +#### Sender Email Verification + +In order for SES to send emails, the sender address must be +[verified](https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html) +prior to the first run of the lambda. + +#### Canary Email Verification + +If the AWS Account is in the SES Sandbox, then recipient addresses will also +need to be +[verified](https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html) +prior to the first run of the lambda. + +#### Canary Run + +Once the needed addresses have been verified, the lambda should be tested with a +canary run by sending to test addresses. + +```yaml +template: + type: http + url: "https://PUBLISH_BUCKET.s3.amazonaws.com/lambda-finops-s3-cost-report/VERSION/lambda-finops-s3-cost-report.yaml" +stack_name: "lambda-finops-s3-cost-report" +stack_tags: + OwnerEmail: "it@sagebase.org" +parameters: + Sender: "it@sagebase.org" + Recipients: "canary1@example.com,canary2@example.com" +``` + +#### Exit SES Sandbox + +Once the sender email address has been verified and a canary run has succeeded, +the AWS account must be move out of the +[SES Sandbox](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html). + +#### Full Deploy + +After moving the AWS account out of the SES Sandbox, redeploy the lambda without +recipient restrictions and with any other needed parameters. + +```yaml +template: + type: http + url: "https://PUBLISH_BUCKET.s3.amazonaws.com/lambda-finops-s3-cost-report/VERSION/lambda-finops-s3-cost-report.yaml" +stack_name: "lambda-finops-s3-cost-report" +stack_tags: + OwnerEmail: "it@sagebase.org" +parameters: + Sender: "it@sagebase.org" + Recipients: "strides-admin@sagebase.org,cloud-audit@sagebase.org" +``` diff --git a/hello_world/app.py b/hello_world/app.py deleted file mode 100644 index 0930620..0000000 --- a/hello_world/app.py +++ /dev/null @@ -1,42 +0,0 @@ -import json - -# import requests - - -def lambda_handler(event, context): - """Sample pure Lambda function - - Parameters - ---------- - event: dict, required - API Gateway Lambda Proxy Input Format - - Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format - - context: object, required - Lambda Context runtime methods and attributes - - Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html - - Returns - ------ - API Gateway Lambda Proxy Output Format: dict - - Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html - """ - - # try: - # ip = requests.get("http://checkip.amazonaws.com/") - # except requests.RequestException as e: - # # Send some context about this error to Lambda Logs - # print(e) - - # raise e - - return { - "statusCode": 200, - "body": json.dumps({ - "message": "hello world", - # "location": ip.text.replace("\n", "") - }), - } diff --git a/hello_world/__init__.py b/s3_cost_report/__init__.py similarity index 100% rename from hello_world/__init__.py rename to s3_cost_report/__init__.py diff --git a/s3_cost_report/app.py b/s3_cost_report/app.py new file mode 100644 index 0000000..5fb921c --- /dev/null +++ b/s3_cost_report/app.py @@ -0,0 +1,197 @@ +import logging +import os +from datetime import datetime + +import boto3 + +from s3_cost_report import ce, ses + +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) + + +iam_client = boto3.client("iam") +sts_client = boto3.client("sts") + + +def report_periods(today): + """ + Calculate the time periods for cost explorer. + + This lambda will run at the beginning of the month, looking at the + previous month and comparing change to the month before that. + + The Start date is inclusive, and the End date is exclusive + """ + target_period = {} + compare_period = {} + + # Special-case the two cases where we cross year boundaries + if today.month == 1: + # in Jan, look at Dec and Nov of last year + target_period["Start"] = f"{today.year - 1}-12-01" + target_period["End"] = f"{today.year}-01-01" + + compare_period["Start"] = f"{today.year - 1}-11-01" + compare_period["End"] = f"{today.year - 1}-12-01" + + elif today.month == 2: + # in Feb, look at Jan of this year and Dec of last year + target_period["Start"] = f"{today.year}-01-01" + target_period["End"] = f"{today.year}-02-01" + + compare_period["Start"] = f"{today.year - 1}-12-01" + compare_period["End"] = f"{today.year}-01-01" + + else: + # no year boundary, look at the previous two months + target_period["Start"] = f"{today.year}-{(today.month - 1):02}-01" + target_period["End"] = f"{today.year}-{today.month:02}-01" + + compare_period["Start"] = f"{today.year}-{(today.month - 2):02}-01" + compare_period["End"] = f"{today.year}-{(today.month - 1):02}-01" + + LOG.info(f"Target month: {target_period}") + LOG.info(f"Compare month: {compare_period}") + + return target_period, compare_period + + +def parse_results_by_time(results_by_time, compare=None): + """ + Transform results returned from Cost Explorer into a useful data structure, + and optionally calculating change from a previous result. + """ + data = {} + + minimum = float(os.environ["MINIMUM"]) + + for result in results_by_time: + for group in result["Groups"]: + amount = float(group["Metrics"][ce.cost_metric]["Amount"]) + if minimum != 0 and amount < minimum: + LOG.warning(f"Skipping amount ({amount}) less than minimum ({minimum})") + continue + + if len(group["Keys"]) != 1: + LOG.error(f"Unexpected grouping: {group['Keys']}") + continue + + key = group["Keys"][0] + data[key] = {"total": amount} + + if compare and key in compare: + _total = data[key]["total"] + _compare = compare[key]["total"] + + # changes from zero are special cases + if _compare == 0: + if _total == 0: + # both are zero, no change + pct = 0 + else: + # up from zero, 100% change + pct = 1 + else: + # calculate percent change + pct = (_total / _compare) - 1 + else: + pct = 1.0 + + data[key]["change"] = pct + + return data + + +def get_service_costs(target_period, compare_period): + """ + Get service cost information from cost explorer for both time periods + and generate a multi-level dictionary. The top-level key will be the + name of the AWS service being summarized, and the subkeys will be the + literal strings 'total' and 'change'; 'total' will map to a float + representing total for this service, and 'change' will map to a float + representing the percent change from the last month. + + Example: + ``` + ec2: + total: 12.3 + change: -0.1 + s3: + total: 32.1 + change: 0.5 + ``` + """ + + # First generate data to compare against + compare_data = ce.get_ce_service_costs(compare_period) + compare_dict = parse_results_by_time(compare_data["ResultsByTime"]) + + # Then generate data our target data, passing in compare data + target_data = ce.get_ce_service_costs(target_period) + target_dict = parse_results_by_time(target_data["ResultsByTime"], compare_dict) + + return target_dict + + +def get_s3_usage_costs(target_period, compare_period): + """ + Get S3 usage cost information from cost explorer for both time periods + and generate a multi-level dictionary. The top-level key will be the + name of the S3 usage type being summarized, and the subkeys will be the + literal strings 'total' and 'change'; 'total' will map to a float + representing total for this service, and 'change' will map to a float + representing the percent change from the last month. + + Example: + ``` + s3-bytes-out: + total: 100.0 + change: 0.5 + s3-timed-storage: + total: 20.0 + change: -0.5 + ``` + """ + + compare_ce_data = ce.get_ce_s3_usage_costs(compare_period) + compare_dict = parse_results_by_time(compare_ce_data["ResultsByTime"]) + + target_ce_data = ce.get_ce_s3_usage_costs(target_period) + target_dict = parse_results_by_time(target_ce_data["ResultsByTime"], compare_dict) + + return target_dict + + +def lambda_handler(event, context): + """ + Entry point + + Send monthly email reports to STRIDES admins with monthly totals for + (1) each AWS service, and (2) each S3 usage type.Include month-over-month + changes for both service and usage-type totals. + """ + + # Get account name (default to ID if no Alias is set) + account = sts_client.get_caller_identity()['Account'] + aliases = iam_client.list_account_aliases()['AccountAliases'] + # aliases will have at most one element + if len(aliases) > 0: + account = aliases[0] + + # Calculate the reporting periods to send to cost explorer + now = datetime.now() + target_month, compare_month = report_periods(now) + + # Build email summary + per_service = get_service_costs(target_month, compare_month) + s3_usage = get_s3_usage_costs(target_month, compare_month) + + # Name of the target period for the email subject + _dt = datetime.fromisoformat(target_month["Start"]) + email_period = _dt.strftime("%B %Y") # Month Year + email_subject = f"AWS Monthly Cost Report ({account} {email_period})" + + # Create and send report + email_html, email_text = ses.build_email_body(per_service, s3_usage) + ses.send_email(email_subject, email_html, email_text) diff --git a/s3_cost_report/ce.py b/s3_cost_report/ce.py new file mode 100644 index 0000000..539843e --- /dev/null +++ b/s3_cost_report/ce.py @@ -0,0 +1,70 @@ +import logging + +import boto3 +from botocore.config import Config as BotoConfig + +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) + +cost_metric = "NetAmortizedCost" + +# Use adaptive mode in an attempt to optimize retry back-off +ce_config = BotoConfig( + retries={ + "mode": "adaptive", # default mode is legacy + } +) +ce_client = boto3.client("ce", config=ce_config) + + +def get_ce_service_costs(period): + """ + Get totals grouped by AWS service + """ + + response = ce_client.get_cost_and_usage( + TimePeriod=period, + Granularity="MONTHLY", + Metrics=[ + cost_metric, + ], + GroupBy=[ + { + "Type": "DIMENSION", + "Key": "SERVICE", + } + ], + ) + + return response + + +def get_ce_s3_usage_costs(period): + """ + Get totals for S3 grouped by usage type + """ + + response = ce_client.get_cost_and_usage( + TimePeriod=period, + Granularity="MONTHLY", + Metrics=[ + cost_metric, + ], + Filter={ + "Dimensions": { + "Key": "SERVICE", + "Values": [ + "Amazon Simple Storage Service", + ], + "MatchOptions": ["EQUALS"], + } + }, + GroupBy=[ + { + "Type": "DIMENSION", + "Key": "USAGE_TYPE", + } + ], + ) + + return response diff --git a/s3_cost_report/ses.py b/s3_cost_report/ses.py new file mode 100644 index 0000000..0f40042 --- /dev/null +++ b/s3_cost_report/ses.py @@ -0,0 +1,264 @@ +import logging +import os + +import boto3 +from botocore.config import Config as BotoConfig +from botocore.exceptions import ClientError + +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) + +# Use standard mode in order to retry on RequestLimitExceeded +# and increase the default number of retries to 10 +ses_config = BotoConfig( + retries={ + "mode": "standard", # default mode is legacy + "max_attempts": 10, # default for standard mode is 3 + } +) +ses_client = boto3.client("ses", config=ses_config) + + +def _table_row_style(i): + """ + Alternating table row background colors. + """ + if i % 2 == 0: + return "style='background-color: WhiteSmoke;'" + else: + return "" + + +def build_paragraph(text, html=False): + """ + Format a text block as a paragraph. + """ + output = "" + + if html: + # Put the paragraph in an invisible table to wrap long lines; + # give the table a single row with two cells, put the text in + # the first cell and let the second cell fill any extra space + output += ( + "" + f"" + "
{text}
" + ) + else: + # Just add a newline + output += text + "\n" + + return output + + +def build_service_table(services, html=False): + """ + Build a table from a dictionary of service totals. + + Example input block: + ``` + ec2: + total: 10.0 + s3: + total: 20.0 + change: 0.5 + ``` + """ + + output = "" + + # Table header + if html: + output += ( + "" + "" + "" + f"" + ) + row_i = 0 # row index for coloring table rows + else: + output += ( + "\t".join(["AWS Service", "Total", "Month-over-Month Change"]) + + "\n" + ) + + # Table rows + for service in services: + # Round dollar total to 2 decimal places + total = f"${services[service]['total']:.2f}" + + change = "" + if "change" in services[service]: + # Convert to a percentage + change = f"{services[service]['change']:.2%}" + + if html: + _td = f"" f"" + + _style = _table_row_style(row_i) + output += f"{_td}" + row_i += 1 + + else: + _td = [service, total, change] + output += "\t".join(_td) + "\n" + + # Table end + if html: + output += "
AWS ServiceTotalMonth-over-Month Change
{service}{total}{change}

" + + return output + + +def build_usage_table(usages, html=False): + """ + Build a table from a dictionary of S3 usage costs + + Example input block: + ``` + s3-bytes-out: + total: 10.0 + s3-timed-storage: + total: 20.0 + change: 0.5 + ``` + """ + + output = "" + total = "Total" + + # Table header + if html: + output += ( + "" + "" + "" + f"" + ) + row_i = 0 # row index for coloring table rows + else: + output += ( + "\t".join(["S3 Usage Type", "Total", "Month-over-Month Change"]) + + "\n" + ) + + # Table rows + for usage_type in usages: + # Round dollar total to 2 decimal places + total = f"${usages[usage_type]['total']:.2f}" + + change = "" + if "change" in usages[usage_type]: + # Convert to a percentage + change = f"{usages[usage_type]['change']:.2%}" + + if html: + _td = ( + f"" + f"" + ) + + _style = _table_row_style(row_i) + output += f"{_td}" + row_i += 1 + + else: + _td = [usage_type, total, change] + output += "\t".join(_td) + "\n" + + # Table end + if html: + output += "
S3 Usage TypeTotalMonth-over-Month Change
{usage_type}{total}{change}

" + + return output + + +def build_email_body(service_data, s3_usage_data): + """ + Compose the email bodies (both a plain-text and HTML version), with + a table for service costs, and a table for S3 usage type costs. + """ + + # Ignore totals under this amount + minimum = float(os.environ["MINIMUM"]) + + service_prose = "\nBreak-down of total monthly costs by service:" + s3_usage_prose = "\nBreak-down of monthly S3 costs by usage type:" + no_data_prose = "\nNo matching data found " + + title = "AWS Monthly Cost Summary" + html_body = f"

{title}

" + text_body = f"{title}\n" + + if service_data: + html_body += build_paragraph(service_prose, True) + text_body += build_paragraph(service_prose, False) + + html_body += build_service_table(service_data, True) + text_body += build_service_table(service_data, False) + else: + no_service_data = f"{no_data_prose} for service totals" + html_body += build_paragraph(no_service_data, True) + text_body += build_paragraph(service_prose, False) + + if s3_usage_data: + html_body += build_paragraph(s3_usage_prose, True) + text_body += build_paragraph(s3_usage_prose, False) + + html_body += build_usage_table(s3_usage_data, True) + text_body += build_usage_table(s3_usage_data, False) + else: + no_s3_usage_data = f"{no_data_prose} for S3 usage totals" + html_body += build_paragraph(no_s3_usage_data, True) + text_body += build_paragraph(service_prose, False) + + LOG.debug(html_body) + LOG.debug(text_body) + return html_body, text_body + + +def send_email(subject, body_html, body_text): + """ + Send an e-mail through SES with both a text body and an HTML body. + """ + + # Sender and Recipients are configured from env vars. + sender = os.environ["SENDER"] + recipients = os.environ["RECIPIENTS"].split(",") + + # Python3 uses UTF-8 + charset = "UTF-8" + + # Send the email. + try: + response = ses_client.send_email( + Destination={ + "ToAddresses": recipients, + }, + Message={ + "Body": { + "Html": { + "Charset": charset, + "Data": body_html, + }, + "Text": { + "Charset": charset, + "Data": body_text, + }, + }, + "Subject": { + "Charset": charset, + "Data": subject, + }, + }, + Source=sender, + ) + + # Display an error if something goes wrong. + except ClientError as e: + LOG.exception(e) + else: + LOG.info(f"Email sent! Message ID: {response['MessageId']}") diff --git a/template.yaml b/template.yaml index a60faad..65e0451 100644 --- a/template.yaml +++ b/template.yaml @@ -1,67 +1,94 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - Hello World lambda + A lambda for sending monthly cloud-spend emails for STRIDES accounts -Metadata: - AWS::ServerlessRepo::Application: - Name: "lambda-template" - Description: "A GH template for quickly starting a new AWS lambda project." - Author: "Sage-Bionetworks" - SpdxLicenseId: "Apache-2.0" - # paths are relative to .aws-sam/build directory - LicenseUrl: "LICENSE" - ReadmeUrl: "README.md" - Labels: ["serverless", "template", "github", "quick-start"] - HomePageUrl: "https://github.com/Sage-Bionetworks-IT/lambda-template" - SemanticVersion: "0.0.3" - SourceCodeUrl: "https://github.com/Sage-Bionetworks-IT/lambda-template/tree/0.0.3" +Parameters: + Sender: + Type: String + Description: Sender email address + + Recipients: + Type: String + Description: Comma-separated list of email recipients + + OmitCostsLessThan: + Type: Number + Description: 'Totals less than this amount will not be reported. Default: $0.01' + Default: '0.01' + + ScheduleExpression: + Type: String + Description: EventBridge Schedule Expression + Default: cron(30 10 2 * ? *) # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: - Timeout: 30 + Timeout: 120 Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction +#lambda execution role config + MonthlyS3UsageLambdaRole: + Type: AWS::IAM::Role Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: 'LambdaSSMAssume' + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: LambdaAccess + PolicyDocument: + Statement: + - Action: + - "ce:Describe*" + - "ce:Get*" + - "ce:List*" + - "iam:ListAccountAliases" + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:DescribeLogStreams" + - "logs:PutLogEvents" + - "ses:SendEmail" + Resource: "*" + Effect: Allow + +# This Lambda will query Cost Explorer for costs related to S3 + MonthlyS3Usage: + Type: AWS::Serverless::Function + Properties: + Handler: s3_cost_report/app.lambda_handler CodeUri: . - Handler: hello_world/app.lambda_handler - Runtime: python3.9 - Role: !GetAtt FunctionRole.Arn + Runtime: python3.12 + MemorySize: 128 + Role: !GetAtt MonthlyS3UsageLambdaRole.Arn + Environment: + Variables: + SENDER: !Ref Sender + RECIPIENTS: !Ref Recipients + MINIMUM: !Ref OmitCostsLessThan Events: - HelloWorld: - Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + ScheduledEventTrigger: + Type: Schedule Properties: - Path: /hello - Method: get + Schedule: !Ref ScheduleExpression - FunctionRole: # execute lambda function with this role - Type: AWS::IAM::Role + LambdaInvokePermission: + Type: AWS::Lambda::Permission Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Action: 'lambda:InvokeFunction' + FunctionName: !Ref MonthlyS3Usage + Principal: 'events.amazonaws.com' Outputs: - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - HelloWorldFunctionArn: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - HelloWorldFunctionRoleArn: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt FunctionRole.Arn + MonthlySe3UsageFunctionArn: + Description: "MonthlyS3Usage Lambda Function ARN" + Value: !GetAtt MonthlyS3Usage.Arn + MonthlyS3UsageFunctionRoleArn: + Description: "IAM Role created for MonthlyS3Usage function" + Value: !GetAtt MonthlyS3UsageLambdaRole.Arn diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..4cfa870 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,172 @@ +import os + +import pytest + +# This needs to be set when the modules are loaded, +# but its value is not used when running tests +os.environ["AWS_DEFAULT_REGION"] = "test-region" +from s3_cost_report import ce + +# Constants used by fixtures + +ce_period = {"Start": "2023-01-01", "End": "2023-02-01"} + +# Set up the test scenario used by all tests +# +# There are three mock service totals: one is typical, the second is lacking +# comparison data, and the third is less than the minimum value to report. +# There are also three mock S3 usage types: the first tests with a zero for the +# previous month, the second tests with a zero for the current month, and the +# third tests with both zeros (simulating fractions of a cent rounded down). + +service1_name = "ec2" +service1_total = 30.0 +service1_previous = 20.0 +service1_change = 0.5 + +service2_name = "s3" +service2_total = 123.45 +service2_change = 1.0 + +service3_name = "lambda" +service3_total = 0.001 +service3_change = 0.0 + +s3_usage_type1_name = "type1" +s3_usage_type1_total = 25.0 +s3_usage_type1_previous = 0.0 +s3_usage_type1_change = 1.0 + +s3_usage_type2_name = "type2" +s3_usage_type2_total = 0.0 +s3_usage_type2_previous = 10 +s3_usage_type2_change = -1.0 + +s3_usage_type3_name = "type3" +s3_usage_type3_total = 0.0 +s3_usage_type3_previous = 0.0 +s3_usage_type3_change = 0.0 + +s3_usage_type4_name = "type4" +s3_usage_type4_total = 1.0 +s3_usage_type4_change = 1.0 + + +# App fixtures + + +@pytest.fixture() +def mock_app_service_dict(): + response = { + service1_name: { + "total": service1_total, + "change": service1_change, + }, + service2_name: { + "total": service2_total, + "change": service2_change, + }, + } + return response + + +@pytest.fixture() +def mock_app_s3_usage_dict(): + response = { + s3_usage_type1_name: { + "total": s3_usage_type1_total, + "change": s3_usage_type1_change, + }, + s3_usage_type2_name: { + "total": s3_usage_type2_total, + "change": s3_usage_type2_change, + }, + s3_usage_type3_name: { + "total": s3_usage_type3_total, + "change": s3_usage_type3_change, + }, + s3_usage_type4_name: { + "total": s3_usage_type4_total, + "change": s3_usage_type4_change + }, + } + return response + + +# CE fixtures + + +def mock_ce_response(data): + groups = [] + + for key, amount in data.items(): + group = { + "Keys": [ + key, + ], + "Metrics": {ce.cost_metric: {"Amount": str(amount)}}, + } + groups.append(group) + + response = { + "GroupDefinitions": [ + {"Type": "DIMENSION", "Key": "SERVICE"}, + ], + "ResultsByTime": [ + {"TimePeriod": ce_period, "Total": {}, "Groups": groups, "Estimated": False} + ], + } + return response + + +@pytest.fixture() +def mock_ce_service_target_data(): + target_totals = { + service1_name: service1_total, + service2_name: service2_total, + service3_name: service3_total, + } + return mock_ce_response(target_totals) + + +@pytest.fixture() +def mock_ce_service_compare_data(): + compare_totals = { + service1_name: service1_previous, + } + return mock_ce_response(compare_totals) + + +@pytest.fixture() +def mock_ce_s3_usage_target_data(): + target_totals = { + s3_usage_type1_name: s3_usage_type1_total, + s3_usage_type2_name: s3_usage_type2_total, + s3_usage_type3_name: s3_usage_type3_total, + s3_usage_type4_name: s3_usage_type4_total, + } + return mock_ce_response(target_totals) + + +@pytest.fixture() +def mock_ce_s3_usage_compare_data(): + compare_totals = { + s3_usage_type1_name: s3_usage_type1_previous, + s3_usage_type2_name: s3_usage_type2_previous, + s3_usage_type3_name: s3_usage_type3_previous, + } + return mock_ce_response(compare_totals) + + +@pytest.fixture() +def mock_ce_period(): + return ce_period + + +# SES fixtures + + +@pytest.fixture() +def mock_ses_response(): + response = {"MessageId": "testId"} + return response diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py new file mode 100644 index 0000000..7b3d7ac --- /dev/null +++ b/tests/unit/test_app.py @@ -0,0 +1,129 @@ +import os +from datetime import datetime + +import pytest +from botocore.stub import Stubber + +from s3_cost_report import app + + +# fixtures for datetime processing around year boundaries + +# in december we target nov of this year +# and compare with oct of this year +test_now_dec = "2020-12-02" + +expected_target_dec = { + "Start": "2020-11-01", + "End": "2020-12-01", +} + +expected_compare_dec = { + "Start": "2020-10-01", + "End": "2020-11-01", +} + +# in january we target dec of last year +# and compare with nov of last year +test_now_jan = "2020-01-02" + +expected_target_jan = { + "Start": "2019-12-01", + "End": "2020-01-01", +} + +expected_compare_jan = { + "Start": "2019-11-01", + "End": "2019-12-01", +} + +# in february we target jan of this year +# and compare with dec of last year +test_now_feb = "2020-02-02" + +expected_target_feb = { + "Start": "2020-01-01", + "End": "2020-02-01", +} + +expected_compare_feb = { + "Start": "2019-12-01", + "End": "2020-01-01", +} + +@pytest.mark.parametrize( + "test_now,expected_target_period,expected_compare_period", + [ + (test_now_dec, expected_target_dec, expected_compare_dec), + (test_now_jan, expected_target_jan, expected_compare_jan), + (test_now_feb, expected_target_feb, expected_compare_feb), + ], +) +def test_report_periods(test_now, expected_target_period, expected_compare_period): + test_dt = datetime.fromisoformat(test_now) + with Stubber(app.sts_client) as _sts: + with Stubber(app.iam_client) as _iam: + found_target, found_compare = app.report_periods(test_dt) + assert found_target == expected_target_period + assert found_compare == expected_compare_period + + +def test_service_costs( + mocker, + mock_ce_period, + mock_ce_service_target_data, + mock_ce_service_compare_data, + mock_app_service_dict, +): + env_vars = { + "MINIMUM": "0.01" + } + mocker.patch.dict(os.environ, env_vars) + + mocker.patch( + "s3_cost_report.ce.get_ce_service_costs", + side_effect=[ + mock_ce_service_compare_data, + mock_ce_service_target_data, + ], + ) + + with Stubber(app.sts_client) as _sts: + with Stubber(app.iam_client) as _iam: + # target and compare periods are passed through to patched functions + found_dict = app.get_service_costs( + mock_ce_period, + mock_ce_period, + ) + assert found_dict == mock_app_service_dict + + +def test_s3_usage_costs( + mocker, + mock_app_s3_usage_dict, + mock_ce_s3_usage_target_data, + mock_ce_s3_usage_compare_data, + mock_ce_period, +): + env_vars = { + "MINIMUM": "0" + } + mocker.patch.dict(os.environ, env_vars) + + mocker.patch( + "s3_cost_report.ce.get_ce_s3_usage_costs", + side_effect=[ + mock_ce_s3_usage_compare_data, + mock_ce_s3_usage_target_data, + ], + ) + + with Stubber(app.sts_client) as _sts: + with Stubber(app.iam_client) as _iam: + + # period input doesn't matter since it's only passed to patched functions + found_dict = app.get_s3_usage_costs( + mock_ce_period, + mock_ce_period, + ) + assert found_dict == mock_app_s3_usage_dict diff --git a/tests/unit/test_ce.py b/tests/unit/test_ce.py new file mode 100644 index 0000000..de3b687 --- /dev/null +++ b/tests/unit/test_ce.py @@ -0,0 +1,26 @@ +import pytest +from botocore.stub import Stubber + +from s3_cost_report import ce + + +def test_ce_service(mock_ce_period, mock_ce_service_target_data): + with Stubber(ce.ce_client) as _stub: + _stub.add_response("get_cost_and_usage", mock_ce_service_target_data) + + # validate our stub response against boto + ce.get_ce_service_costs(mock_ce_period) + + # assert that the client function was called + _stub.assert_no_pending_responses() + + +def test_ce_s3_usage(mock_ce_period, mock_ce_s3_usage_target_data): + with Stubber(ce.ce_client) as _stub: + _stub.add_response("get_cost_and_usage", mock_ce_s3_usage_target_data) + + # validate our stub response against boto + ce.get_ce_s3_usage_costs(mock_ce_period) + + # assert that the client function was called + _stub.assert_no_pending_responses() diff --git a/tests/unit/test_handler.py b/tests/unit/test_handler.py deleted file mode 100644 index 09588d3..0000000 --- a/tests/unit/test_handler.py +++ /dev/null @@ -1,73 +0,0 @@ -import json - -import pytest - -from hello_world import app - - -@pytest.fixture() -def apigw_event(): - """ Generates API GW Event""" - - return { - "body": '{ "test": "body"}', - "resource": "/{proxy+}", - "requestContext": { - "resourceId": "123456", - "apiId": "1234567890", - "resourcePath": "/{proxy+}", - "httpMethod": "POST", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "accountId": "123456789012", - "identity": { - "apiKey": "", - "userArn": "", - "cognitoAuthenticationType": "", - "caller": "", - "userAgent": "Custom User Agent String", - "user": "", - "cognitoIdentityPoolId": "", - "cognitoIdentityId": "", - "cognitoAuthenticationProvider": "", - "sourceIp": "127.0.0.1", - "accountId": "", - }, - "stage": "prod", - }, - "queryStringParameters": {"foo": "bar"}, - "headers": { - "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", - "Accept-Language": "en-US,en;q=0.8", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Mobile-Viewer": "false", - "X-Forwarded-For": "127.0.0.1, 127.0.0.2", - "CloudFront-Viewer-Country": "US", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Upgrade-Insecure-Requests": "1", - "X-Forwarded-Port": "443", - "Host": "1234567890.execute-api.us-east-1.amazonaws.com", - "X-Forwarded-Proto": "https", - "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", - "CloudFront-Is-Tablet-Viewer": "false", - "Cache-Control": "max-age=0", - "User-Agent": "Custom User Agent String", - "CloudFront-Forwarded-Proto": "https", - "Accept-Encoding": "gzip, deflate, sdch", - }, - "pathParameters": {"proxy": "/examplepath"}, - "httpMethod": "POST", - "stageVariables": {"baz": "qux"}, - "path": "/examplepath", - } - - -def test_lambda_handler(apigw_event, mocker): - - ret = app.lambda_handler(apigw_event, "") - data = json.loads(ret["body"]) - - assert ret["statusCode"] == 200 - assert "message" in ret["body"] - assert data["message"] == "hello world" - # assert "location" in data.dict_keys() diff --git a/tests/unit/test_ses.py b/tests/unit/test_ses.py new file mode 100644 index 0000000..54c607b --- /dev/null +++ b/tests/unit/test_ses.py @@ -0,0 +1,38 @@ +import os + +import pytest +from botocore.stub import Stubber + +from s3_cost_report import ses + + +def test_send_email(mocker, mock_ses_response): + text_body = "test" + html_body = "test" + subject = "Report: Test Month" + + env_vars = { + "RECIPIENTS": "admin@example.com,cc@example.com", + "SENDER": "test@example.com", + } + mocker.patch.dict(os.environ, env_vars) + + with Stubber(ses.ses_client) as _stub: + _stub.add_response("send_email", mock_ses_response) + + ses.send_email(subject, html_body, text_body) + + # assert that the client function was called + _stub.assert_no_pending_responses() + + +def test_email_body(mocker, mock_app_service_dict, mock_app_s3_usage_dict): + # assert no exceptions are raised + env_vars = { + "MINIMUM": "0.01" + } + mocker.patch.dict(os.environ, env_vars) + + html, text = ses.build_email_body(mock_app_service_dict, mock_app_s3_usage_dict) + print(html) + print(text)