├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── Makefile ├── README.md ├── app.py ├── cdk.json ├── cdk ├── __init__.py └── stack.py ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── scripts ├── docker └── make-deps.sh ├── services ├── __init__.py ├── event_api │ ├── infrastructure.py │ └── runtime │ │ ├── adapters │ │ ├── __init__.py │ │ ├── file_storage.py │ │ ├── message.py │ │ ├── ports │ │ │ ├── __init__.py │ │ │ ├── file_port.py │ │ │ └── message_port.py │ │ └── service.py │ │ └── api.py └── event_processor │ ├── infrastructure.py │ └── runtime │ ├── adapters │ ├── __init__.py │ ├── ports │ │ ├── __init__.py │ │ └── event_store.py │ ├── service.py │ └── table_store.py │ └── processor.py └── tests ├── __init__.py ├── e2e └── api │ ├── __init__.py │ ├── conftest.py │ ├── infrastructure.py │ └── test_api.py ├── events ├── event_api_gw.json └── event_processor_sqs.json ├── integration ├── __init__.py ├── event_api │ ├── __init__.py │ ├── conftest.py │ ├── infrastructure.py │ └── test_event_api.py └── event_processor │ ├── __init__.py │ ├── conftest.py │ ├── infrastructure.py │ └── test_event_processor.py ├── unit ├── adapters │ ├── event_store.py │ ├── file_port.py │ └── message_port.py ├── test_event_processor.py └── test_event_service.py └── utils ├── __init__.py ├── base_infrastructure.py ├── constants.py ├── fetcher.py └── provider.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, W503, BLK100, W291, I004 3 | max-line-length = 120 4 | max-complexity = 15 5 | 6 | [isort] 7 | multi_line_output = 3 8 | include_trailing_comma = true 9 | force_grid_wrap = 0 10 | use_parentheses = true 11 | line_length = 120 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | # Must ignore requirements.txt - these will be generated 5 | requirements.txt 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### OSX ### 23 | # General 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### PyCharm ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff 55 | .idea/**/workspace.xml 56 | .idea/**/tasks.xml 57 | .idea/**/usage.statistics.xml 58 | .idea/**/dictionaries 59 | .idea/**/shelf 60 | 61 | # Generated files 62 | .idea/**/contentModel.xml 63 | 64 | # Sensitive or high-churn files 65 | .idea/**/dataSources/ 66 | .idea/**/dataSources.ids 67 | .idea/**/dataSources.local.xml 68 | .idea/**/sqlDataSources.xml 69 | .idea/**/dynamic.xml 70 | .idea/**/uiDesigner.xml 71 | .idea/**/dbnavigator.xml 72 | 73 | # Gradle 74 | .idea/**/gradle.xml 75 | .idea/**/libraries 76 | 77 | # Gradle and Maven with auto-import 78 | # When using Gradle or Maven with auto-import, you should exclude module files, 79 | # since they will be recreated, and may cause churn. Uncomment if using 80 | # auto-import. 81 | # .idea/modules.xml 82 | # .idea/*.iml 83 | # .idea/modules 84 | # *.iml 85 | # *.ipr 86 | 87 | # CMake 88 | cmake-build-*/ 89 | 90 | # Mongo Explorer plugin 91 | .idea/**/mongoSettings.xml 92 | 93 | # File-based project format 94 | *.iws 95 | 96 | # IntelliJ 97 | out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Cursive Clojure plugin 106 | .idea/replstate.xml 107 | 108 | # Crashlytics plugin (for Android Studio and IntelliJ) 109 | com_crashlytics_export_strings.xml 110 | crashlytics.properties 111 | crashlytics-build.properties 112 | fabric.properties 113 | 114 | # Editor-based Rest Client 115 | .idea/httpRequests 116 | 117 | # Android studio 3.1+ serialized cache file 118 | .idea/caches/build_file_checksums.ser 119 | 120 | ### PyCharm Patch ### 121 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 122 | 123 | # *.iml 124 | # modules.xml 125 | # .idea/misc.xml 126 | # *.ipr 127 | 128 | # Sonarlint plugin 129 | .idea/sonarlint 130 | 131 | ### Python ### 132 | # Byte-compiled / optimized / DLL files 133 | __pycache__/ 134 | *.py[cod] 135 | *$py.class 136 | 137 | # C extensions 138 | *.so 139 | 140 | # Distribution / packaging 141 | .Python 142 | build/ 143 | develop-eggs/ 144 | dist/ 145 | downloads/ 146 | eggs/ 147 | .eggs/ 148 | lib/ 149 | lib64/ 150 | parts/ 151 | sdist/ 152 | var/ 153 | wheels/ 154 | pip-wheel-metadata/ 155 | share/python-wheels/ 156 | *.egg-info/ 157 | .installed.cfg 158 | *.egg 159 | MANIFEST 160 | 161 | # PyInstaller 162 | # Usually these files are written by a python script from a template 163 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 164 | *.manifest 165 | *.spec 166 | 167 | # Installer logs 168 | pip-log.txt 169 | pip-delete-this-directory.txt 170 | 171 | # Unit test / coverage reports 172 | htmlcov/ 173 | .tox/ 174 | .nox/ 175 | .coverage 176 | .coverage.* 177 | .cache 178 | nosetests.xml 179 | coverage.xml 180 | *.cover 181 | .hypothesis/ 182 | .pytest_cache/ 183 | 184 | # Translations 185 | *.mo 186 | *.pot 187 | 188 | # Django stuff: 189 | *.log 190 | local_settings.py 191 | db.sqlite3 192 | db.sqlite3-journal 193 | 194 | # Flask stuff: 195 | instance/ 196 | .webassets-cache 197 | 198 | # Scrapy stuff: 199 | .scrapy 200 | 201 | # Sphinx documentation 202 | docs/_build/ 203 | 204 | # PyBuilder 205 | target/ 206 | 207 | # Jupyter Notebook 208 | .ipynb_checkpoints 209 | 210 | # IPython 211 | profile_default/ 212 | ipython_config.py 213 | 214 | # pyenv 215 | .python-version 216 | 217 | # pipenv 218 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 219 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 220 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 221 | # install all needed dependencies. 222 | #Pipfile.lock 223 | 224 | # celery beat schedule file 225 | celerybeat-schedule 226 | 227 | # SageMath parsed files 228 | *.sage.py 229 | 230 | # Environments 231 | .env 232 | .venv 233 | env/ 234 | venv/ 235 | ENV/ 236 | env.bak/ 237 | venv.bak/ 238 | 239 | # Spyder project settings 240 | .spyderproject 241 | .spyproject 242 | 243 | # Rope project settings 244 | .ropeproject 245 | 246 | # mkdocs documentation 247 | /site 248 | 249 | # mypy 250 | .mypy_cache/ 251 | .dmypy.json 252 | dmypy.json 253 | 254 | # Pyre type checker 255 | .pyre/ 256 | 257 | ### VisualStudioCode ### 258 | .vscode 259 | .vscode/* 260 | !.vscode/tasks.json 261 | !.vscode/launch.json 262 | !.vscode/extensions.json 263 | 264 | ### VisualStudioCode Patch ### 265 | # Ignore all local history of files 266 | .history 267 | 268 | ### Windows ### 269 | # Windows thumbnail cache files 270 | Thumbs.db 271 | Thumbs.db:encryptable 272 | ehthumbs.db 273 | ehthumbs_vista.db 274 | 275 | # Dump file 276 | *.stackdump 277 | 278 | # Folder config file 279 | [Dd]esktop.ini 280 | 281 | # Recycle Bin used on file shares 282 | $RECYCLE.BIN/ 283 | 284 | # Windows Installer files 285 | *.cab 286 | *.msi 287 | *.msix 288 | *.msm 289 | *.msp 290 | 291 | # Windows shortcuts 292 | *.lnk 293 | 294 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 295 | 296 | # Misc 297 | test_report 298 | wheelhouse 299 | /.idea/* 300 | *.html 301 | TMP_CHANGELOG.md 302 | 303 | # Docs files 304 | docs/.cache/ 305 | docs/public 306 | node_modules 307 | /api/ 308 | site/ 309 | !404.html 310 | !docs/overrides/*.html 311 | 312 | !.github/workflows/lib 313 | examples/**/sam/.aws-sam 314 | 315 | # cdk 316 | cdk.out 317 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: local 12 | hooks: 13 | - id: isort 14 | name: foramtting::isort 15 | entry: poetry run isort 16 | language: system 17 | types: [python] 18 | - id: black 19 | name: foramtting::black 20 | entry: poetry run black 21 | language: system 22 | types: [python] 23 | - repo: local 24 | hooks: 25 | - id: flake8 26 | name: linting::flake8 27 | entry: poetry run flake8 28 | language: system 29 | types: [python] 30 | - repo: https://github.com/aws-cloudformation/cfn-python-lint 31 | rev: v0.61.1 32 | hooks: 33 | - id: cfn-python-lint 34 | files: template.yaml 35 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable= 3 | too-many-arguments, 4 | too-many-instance-attributes, 5 | too-few-public-methods, 6 | anomalous-backslash-in-string, 7 | missing-class-docstring, 8 | missing-module-docstring, 9 | missing-function-docstring, 10 | 11 | [FORMAT] 12 | max-line-length=120 13 | 14 | [MASTER] 15 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export WORKDIR = $(shell pwd) 2 | 3 | project=services 4 | cdk_src = cdk 5 | service_src = services 6 | tests_src = tests 7 | 8 | e2e_tests = $(tests_src)/e2e 9 | int_tests = $(tests_src)/integration 10 | all_src = $(cdk_src) $(service_src) $(tests_src) 11 | 12 | .PHONY: target 13 | target: 14 | @$(MAKE) pr 15 | 16 | .PHONY: dev 17 | dev: 18 | pip install --upgrade pip pre-commit poetry 19 | poetry install 20 | pre-commit install 21 | 22 | .PHONY: tests 23 | tests: 24 | poetry run pytest --ignore $(e2e_tests) --ignore $(int_tests) --cov=$(project) --cov-report=xml --cov-report term 25 | 26 | .PHONY: tests/integration 27 | tests/integration: tests deps 28 | poetry run pytest $(int_tests) --cov=$(project) --cov-report=xml --cov-report term 29 | 30 | .PHONY: tests/e2e 31 | tests/e2e: tests deps 32 | poetry run pytest $(e2e_tests) --cov=$(project) --cov-report=xml --cov-report term 33 | 34 | .PHONY: format 35 | format: 36 | poetry run isort $(all_src) 37 | poetry run black $(all_src) 38 | 39 | .PHONY: lint 40 | lint: format 41 | poetry run flake8 $(all_src) 42 | 43 | .PHONY: pre-commit 44 | pre-commit: 45 | pre-commit run --show-diff-on-failure 46 | 47 | .PHONY: pr 48 | pr: lint mypy pre-commit test 49 | 50 | .PHONY: synth 51 | synth: deps 52 | poetry run cdk synth 53 | 54 | .PHONY: deploy 55 | deploy: deps 56 | poetry run cdk deploy 57 | 58 | .PHONY: deploy/diff 59 | deploy/diff: deps 60 | poetry run cdk diff 61 | 62 | .PHONY: deploy/remove 63 | deploy/remove: 64 | poetry run cdk destroy 65 | 66 | .PHONY: deploy/destroy 67 | deploy/destroy: 68 | poetry run cdk destroy 69 | 70 | .PHONY: deps 71 | deps: 72 | scripts/make-deps.sh 73 | 74 | .PHONY: clean 75 | clean: 76 | rm -rf cdk.out .vscode .pytest_cache .coverage coverage.xml .mypy_cache 77 | find services -type f -name "requirements.txt" -delete 78 | find services cdk tests -type d -name "__pycache__" -exec rm -rf {} \; 79 | poetry env remove --all 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploying Python code to AWS Lambda with CDK 2 | 3 | Deploying Python code to AWS Lambda using the AWS CDK is confusing especially when there are so many different methods and options available. I honestly feel right now the true AWS Serverless *Best Practice* is yet to fully emerge so I tend to tread carefully and make the choices that work for my teams and projects. I also tend to move slowly with adopting the next BIG thing and make sure I play with and understand the choices I am making. So I have put this project example together for my own use and to use as a basis for Serverless teams that I assist and accelerate in my day to day work life, it has the things that I like and prefer to use and understand. 4 | 5 | There are a few considerations when building out a project directory structure for a Serverless Service built around AWS Lambda packaged and deployed using the AWS CDK. 6 | 7 | **Important Note:** This repo is a work in progress and under constant churn right now as I work to complete a full end to end solution of dev, package, test and deploy. 8 | 9 | ## Project Structure Considerations 10 | 11 | 1. Lambda function code must be usable in test suite files. 12 | 2. Lambda service functions must be able to use relative code modules and must remain executable in both test suites and by the AWS Lambda runtime. 13 | 3. Lambda functions must only include the dependencies they directly use so that lambda function sizes are kept to a minimum to ease cold start time pressure. 14 | 4. CDK infrastructure for Lambda service components must be co-located with the lambda function to ease refactoring and ensure related service elements are kept together in a single unit 15 | 5. CDK Infrastructure for each service component must be implemented as a CDK Construct and not a stack. This will enable faster refactoring and enable flexible stack creation and re-use in real cloud testing. 16 | 6. CDK Constructs should reference real folders and the "cdk deploy" should be the thing that does all the work of packaging and deploying so there are no hidden steps. 17 | 7. Sometimes 6 is not always achievable and I have added some "magic" in the requirements.txt creation which is explained further down in the README around my use of poetry for dependency management. 18 | 19 | ## Hexagonal Architecture 20 | 21 | There has been a lot of talk about [Hexagonal Architectures](https://alistair.cockburn.us/hexagonal-architecture/) in the AWS Serverless world this year and I have written articles on this topic which you can find on [my blog](https://blog.walmsles.io). In this project I use the Dependency Inversion principle in building out Ports and Adapters to create loosely coupled classes for accessing AWS Cloud resources. 22 | 23 | This removes the need for mocking AWS SDK calls to test the Microservice code you are writing which is often hard or problematic for developers new to AWS Serverless. Although we are eliminating Mocks for SDK calls, you still need mocks (or fakes) for each of your adapters - but you won't need any mocking libraries or need to worry about how your code calls the SDK - not for unit tests anyway! This will enable you and your team to move faster. 24 | 25 | Hold on! If you don't mock AWS SDK calls - how am I going to test that writing a file to S3 works? Well - we do that here in Integration tests which push up cloud infrastructure and trigger your Lambda directly with fixtures representing the integration event that triggers them - in this way you are testing how the real adapters call the AWS SDK by validating the outcome with real instances of the services - no need to mock when you are in the cloud. 26 | 27 | I love zero AWS SDK mocking! It makes more sense to developers and everyone is happier! 28 | 29 | I am using this repo to also explore this concept and the ideas presented here will change as I mature my understanding through building and testing. 30 | 31 | ## Where to Start? 32 | 33 | I have based this project structure on the AWS Sample repo [aws-cdk-project-structure-python](https://github.com/aws-samples/aws-cdk-project-structure-python) on GitHub which is a great starting point. It uses the [aws-cdk-aws-lambda-python-alpha](https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda_python_alpha.html) construct for deploying Lambda code. 34 | 35 | I have also borrowed some project structure and management ideas directly from [aws-lambda-powertools](https://github.com/awslabs/aws-lambda-powertools-python) for python which is a great example of an Open Source repo for a python project! 36 | 37 | **NOTE:** The first time you execute a synth or deploy takes a long time. There is no output letting you know what is going on while the docker images are being downloaded to perform the initial packaging (See [Github Issue](https://github.com/aws/aws-cdk/issues/20390)) - once this is done synth and deploy are much faster, so do hang in there for the first synth/deploy. 38 | 39 | I have made the following changes/additions: 40 | 41 | - **poetry** used for packaging instead of pip 42 | - All requirements are managed at the root level including runtime dependencies for Lambda functions to be deployed. 43 | - added **Makefile** to simplify the management of the project. 44 | - relocated stack and toolchain files to **cdk** folder to keep the root folder cleaner. 45 | - Added some useful conventions (see below) to enable new developers to understand more clearly where things live when adding to a project. 46 | - changed how lambda runtime **requirements.txt** files work - these are now generated from the central poetry dependencies. 47 | 48 | ## Useful Conventions 49 | 50 | The following conventions are used in this project setup and must be adhered to for all the processes to work correctly. The conventions are straight-forward and intended to add logical process over the projects. 51 | 52 | ### Project Folders 53 | 54 | #### cdk 55 | 56 | This folder is where common cdk modules should exist along with the CDK Stack to be deployed by the service. The **stack.py** is the file to modify and include all your constructs from the services folder. 57 | 58 | #### tests 59 | 60 | This folder contains pytest files for testing your service code and is broken down into: 61 | 62 | - Unit - stand-alone unit test suites testing individual code modules and functions 63 | - Integration - Tests which create cloud infrastructure based on the construct defined by the service. 64 | - E2E - End to end tests to validate the service is working. 65 | 66 | #### services 67 | 68 | This folder is where each of your micro-service components is defined. Each folder should represent a complete component of your solution. The folder names are important here as they will tie into how Python dependencies are packaged for each micro-service (see Poetry section below). Service folder structure use the following: 69 | 70 | ``` 71 | ├── infrastructure.py 72 | └── runtime 73 | ├── __init__.py 74 | ├── adapters 75 | │ ├── __init__.py 76 | │ ├── fake_storage.py 77 | │ ├── file_storage.py 78 | │ └── ports 79 | │ ├── __init__.py 80 | │ ├── event_port.py 81 | │ └── file_port.py 82 | ├── api.py 83 | ├── event.py 84 | └── requirements.txt 85 | ``` 86 | - **infrastructure.py** contains the CDK construct code for the service. This should always be a construct and not a stack since this enables a single stack to be created for a service which I feel is critical. Thsi also enables a component to be split-out and re-used pretty quickly as you have built for this already. 87 | - **runtime** contains the actual Lambda code for the service (if Lamda is being used) and is the folder that the **PythonFunction** construct packages for you. teh benefit of using this alpha feature is proper Lambda packaging is done in a Lambda compatible environment according to your chosen Architecture. 88 | - **adapters** is used to house adapter implementation for the Service - we are using Hexagonal Achitecture here and ports and adapters are critical in enabling cloud isolation and simpler testing. 89 | - **adapters/ports** are the interfaces used by the adapter implementations. 90 | 91 | ### Poetry - Managing Python Dependencies 92 | 93 | I like poetry for managing dependencies, in particular I like the **group** option for [adding dependencies](https://python-poetry.org/docs/cli/#add). A key need is to ensure that each Lambda deployment package is as small as possible. Using poetry all dependecies for each service can be added to a **group** for each service within the **service** folder, this enables the `make deps` target to look for the **runtime** folders and perform a `poetry export --with=group-name` to create the **requirements.txt** for packaging by the *8PythonLambda** construct. 94 | 95 | There is a `make-deps.sh` bash script that is used to generate the requirements.txt files for each runtime folder. 96 | 97 | When adding dependencies for a **service** with a runtime folder in the `service/event_api` folder you can use: 98 | 99 | ``` 100 | $ poetry add aws-lambda-powertools --group=event_api 101 | ``` 102 | 103 | The same dependency can be added to multiple groups and this is okay - poetry takes care of this for you. The advantage is you get a customised **requirements.txt** for each service with only thier dependencies and the tooling in the repo will naturally deal with new services. 104 | 105 | 106 | 107 | # Using Podman in place of Docker 108 | 109 | The AWS CDK **PythonFunction** construct requires docker in order to package the lambda. I am working in an enterprise environment and am not enabled to install the docker desktop (I do not use it enough to qualify for a funding code to license the desktop tool). 110 | 111 | Instead I use podman on my mac which can be used as a drop-in replacement to docker. In order to use `podman` you will need the following installed: 112 | 113 | - [Homebrew](https://brew.sh) 114 | - [Podman](https://podman.io) 115 | - `scripts/docker` placed into your local env path 116 | 117 | The `docker` script pushes all commands straight through to podman and enables AWS CDK PythonFunction to do a full deployment. 118 | 119 | **WARNING:** Using `podman` or any other open source "Docker compatible" project comes with risks that CDK packaging will fail when the docker image is updated. For AWS CDK, Docker will always work, and I would recommend using Docker when you can for this reason. When the docker image stopped working with podman I also tried `colima` and `minikube` which are also viable replacements but they all displayed similar behaviour. 120 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # import os 3 | 4 | from aws_cdk import App 5 | 6 | from cdk.stack import AppStack 7 | 8 | app = App() 9 | AppStack( 10 | app, 11 | "AppStack", 12 | ) 13 | 14 | app.synth() 15 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements.txt", 11 | "source.bat", 12 | "python/__pycache__", 13 | "__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/core:checkSecretUsage": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 33 | "@aws-cdk/core:target-partitions": [ 34 | "aws", 35 | "aws-cn" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/cdk/__init__.py -------------------------------------------------------------------------------- /cdk/stack.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import typing 3 | from pathlib import Path 4 | 5 | from aws_cdk import Environment, IStackSynthesizer, Stack 6 | from constructs import Construct 7 | 8 | from services.event_api.infrastructure import EventApiConstruct 9 | from services.event_processor.infrastructure import EventProcessorConstruct 10 | 11 | CDK_PACKAGE_PATH = Path(__file__).parent 12 | 13 | 14 | class AppStack(Stack): 15 | def __init__( 16 | self, 17 | scope: typing.Optional[Construct] = None, 18 | id: typing.Optional[builtins.str] = None, 19 | *, 20 | analytics_reporting: typing.Optional[builtins.bool] = None, 21 | description: typing.Optional[builtins.str] = None, 22 | env: typing.Optional[ 23 | typing.Union[Environment, typing.Dict[str, typing.Any]] 24 | ] = None, 25 | stack_name: typing.Optional[builtins.str] = None, 26 | synthesizer: typing.Optional[IStackSynthesizer] = None, 27 | tags: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None, 28 | termination_protection: typing.Optional[builtins.bool] = None 29 | ) -> None: 30 | super().__init__( 31 | scope, 32 | id, 33 | analytics_reporting=analytics_reporting, 34 | description=description, 35 | env=env, 36 | stack_name=stack_name, 37 | synthesizer=synthesizer, 38 | tags=tags, 39 | termination_protection=termination_protection, 40 | ) 41 | 42 | event_construct = EventApiConstruct(self, "EventApi") 43 | EventProcessorConstruct( 44 | self, "EventProcessor", queue=event_construct.event_api.queue 45 | ) 46 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any=False 3 | warn_unused_configs=True 4 | no_implicit_optional=True 5 | warn_redundant_casts=True 6 | warn_unused_ignores=True 7 | show_column_numbers = True 8 | show_error_codes = True 9 | show_error_context = True 10 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "astroid" 5 | version = "2.15.6" 6 | description = "An abstract syntax tree for Python with inference support." 7 | optional = false 8 | python-versions = ">=3.7.2" 9 | files = [ 10 | {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, 11 | {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, 12 | ] 13 | 14 | [package.dependencies] 15 | lazy-object-proxy = ">=1.4.0" 16 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 17 | wrapt = [ 18 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 19 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 20 | ] 21 | 22 | [[package]] 23 | name = "attrs" 24 | version = "23.1.0" 25 | description = "Classes Without Boilerplate" 26 | optional = false 27 | python-versions = ">=3.7" 28 | files = [ 29 | {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, 30 | {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, 31 | ] 32 | 33 | [package.extras] 34 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 35 | dev = ["attrs[docs,tests]", "pre-commit"] 36 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 37 | tests = ["attrs[tests-no-zope]", "zope-interface"] 38 | tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 39 | 40 | [[package]] 41 | name = "aws-cdk-asset-awscli-v1" 42 | version = "2.2.200" 43 | description = "A library that contains the AWS CLI for use in Lambda Layers" 44 | optional = false 45 | python-versions = "~=3.7" 46 | files = [ 47 | {file = "aws-cdk.asset-awscli-v1-2.2.200.tar.gz", hash = "sha256:af4d67ef7aa4183073e63be5f88d1ce1912b24d2ebac35148e84678d674bdfcd"}, 48 | {file = "aws_cdk.asset_awscli_v1-2.2.200-py3-none-any.whl", hash = "sha256:ed1b881402b255daec151e386581a627ce13f4d5cb94b7184e6efc38d27584b0"}, 49 | ] 50 | 51 | [package.dependencies] 52 | jsii = ">=1.84.0,<2.0.0" 53 | publication = ">=0.0.3" 54 | typeguard = ">=2.13.3,<2.14.0" 55 | 56 | [[package]] 57 | name = "aws-cdk-asset-kubectl-v20" 58 | version = "2.1.2" 59 | description = "A library that contains kubectl for use in Lambda Layers" 60 | optional = false 61 | python-versions = "~=3.7" 62 | files = [ 63 | {file = "aws-cdk.asset-kubectl-v20-2.1.2.tar.gz", hash = "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164"}, 64 | {file = "aws_cdk.asset_kubectl_v20-2.1.2-py3-none-any.whl", hash = "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"}, 65 | ] 66 | 67 | [package.dependencies] 68 | jsii = ">=1.70.0,<2.0.0" 69 | publication = ">=0.0.3" 70 | typeguard = ">=2.13.3,<2.14.0" 71 | 72 | [[package]] 73 | name = "aws-cdk-asset-node-proxy-agent-v5" 74 | version = "2.0.166" 75 | description = "@aws-cdk/asset-node-proxy-agent-v5" 76 | optional = false 77 | python-versions = "~=3.7" 78 | files = [ 79 | {file = "aws-cdk.asset-node-proxy-agent-v5-2.0.166.tar.gz", hash = "sha256:73d4a4f4bdeb3019779137e4b18d7c5efc8353dff673508bd8c584569ea18db6"}, 80 | {file = "aws_cdk.asset_node_proxy_agent_v5-2.0.166-py3-none-any.whl", hash = "sha256:d71d80710734c47f7114e3e51843c4a8ebae3bbe5032a5af40ca56ca611c0cfe"}, 81 | ] 82 | 83 | [package.dependencies] 84 | jsii = ">=1.85.0,<2.0.0" 85 | publication = ">=0.0.3" 86 | typeguard = ">=2.13.3,<2.14.0" 87 | 88 | [[package]] 89 | name = "aws-cdk-aws-lambda-python-alpha" 90 | version = "2.89.0a0" 91 | description = "The CDK Construct Library for AWS Lambda in Python" 92 | optional = false 93 | python-versions = "~=3.7" 94 | files = [ 95 | {file = "aws-cdk.aws-lambda-python-alpha-2.89.0a0.tar.gz", hash = "sha256:e974653db17779fdeba214528ea73a26fcdabc140991af3c9f3bf3d6e707f54e"}, 96 | {file = "aws_cdk.aws_lambda_python_alpha-2.89.0a0-py3-none-any.whl", hash = "sha256:0a4454b8e8670832e01935e430015bc8ab7690fa0a23b052c6abc44931ac3ede"}, 97 | ] 98 | 99 | [package.dependencies] 100 | aws-cdk-lib = "2.89.0" 101 | constructs = ">=10.0.0,<11.0.0" 102 | jsii = ">=1.85.0,<2.0.0" 103 | publication = ">=0.0.3" 104 | typeguard = ">=2.13.3,<2.14.0" 105 | 106 | [[package]] 107 | name = "aws-cdk-lib" 108 | version = "2.89.0" 109 | description = "Version 2 of the AWS Cloud Development Kit library" 110 | optional = false 111 | python-versions = "~=3.7" 112 | files = [ 113 | {file = "aws-cdk-lib-2.89.0.tar.gz", hash = "sha256:8fbd1d4ee0ffeb67bcc845bef5a10575dbc678ad07f74cdb3cb4243afc433db7"}, 114 | {file = "aws_cdk_lib-2.89.0-py3-none-any.whl", hash = "sha256:92eeebd77fe17b36029fae20f46eb601710485ea7c808c3d33fe1c71fee125bd"}, 115 | ] 116 | 117 | [package.dependencies] 118 | "aws-cdk.asset-awscli-v1" = ">=2.2.200,<3.0.0" 119 | "aws-cdk.asset-kubectl-v20" = ">=2.1.2,<3.0.0" 120 | "aws-cdk.asset-node-proxy-agent-v5" = ">=2.0.166,<3.0.0" 121 | constructs = ">=10.0.0,<11.0.0" 122 | jsii = ">=1.85.0,<2.0.0" 123 | publication = ">=0.0.3" 124 | typeguard = ">=2.13.3,<2.14.0" 125 | 126 | [[package]] 127 | name = "aws-lambda-powertools" 128 | version = "2.22.0" 129 | description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." 130 | optional = false 131 | python-versions = ">=3.7.4,<4.0.0" 132 | files = [ 133 | {file = "aws_lambda_powertools-2.22.0-py3-none-any.whl", hash = "sha256:eae1f1c961893dab5d1e75ffb44d9b58f6426cb148aa39413b04cf36ae46fbe3"}, 134 | {file = "aws_lambda_powertools-2.22.0.tar.gz", hash = "sha256:0fd535251454b1bd68dbff65e3ed56aa567f3841011e2afbd557b125596a6814"}, 135 | ] 136 | 137 | [package.dependencies] 138 | typing-extensions = ">=4.6.2,<5.0.0" 139 | 140 | [package.extras] 141 | all = ["aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "pydantic (>=1.8.2,<2.0.0)"] 142 | aws-sdk = ["boto3 (>=1.20.32,<2.0.0)"] 143 | parser = ["pydantic (>=1.8.2,<2.0.0)"] 144 | tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] 145 | validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] 146 | 147 | [[package]] 148 | name = "black" 149 | version = "22.12.0" 150 | description = "The uncompromising code formatter." 151 | optional = false 152 | python-versions = ">=3.7" 153 | files = [ 154 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 155 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 156 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 157 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 158 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 159 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 160 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 161 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 162 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 163 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 164 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 165 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 166 | ] 167 | 168 | [package.dependencies] 169 | click = ">=8.0.0" 170 | mypy-extensions = ">=0.4.3" 171 | pathspec = ">=0.9.0" 172 | platformdirs = ">=2" 173 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 174 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 175 | 176 | [package.extras] 177 | colorama = ["colorama (>=0.4.3)"] 178 | d = ["aiohttp (>=3.7.4)"] 179 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 180 | uvloop = ["uvloop (>=0.15.2)"] 181 | 182 | [[package]] 183 | name = "boto3" 184 | version = "1.28.15" 185 | description = "The AWS SDK for Python" 186 | optional = false 187 | python-versions = ">= 3.7" 188 | files = [ 189 | {file = "boto3-1.28.15-py3-none-any.whl", hash = "sha256:84b7952858e9319968b0348d9894a91a6bb5f31e81a45c68044d040a12362abe"}, 190 | {file = "boto3-1.28.15.tar.gz", hash = "sha256:a6e711e0b6960c3a5b789bd30c5a18eea7263f2a59fc07f85efa5e04804e49d2"}, 191 | ] 192 | 193 | [package.dependencies] 194 | botocore = ">=1.31.15,<1.32.0" 195 | jmespath = ">=0.7.1,<2.0.0" 196 | s3transfer = ">=0.6.0,<0.7.0" 197 | 198 | [package.extras] 199 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 200 | 201 | [[package]] 202 | name = "boto3-stubs" 203 | version = "1.28.15.post1" 204 | description = "Type annotations for boto3 1.28.15 generated with mypy-boto3-builder 7.16.2" 205 | optional = false 206 | python-versions = ">=3.7" 207 | files = [ 208 | {file = "boto3-stubs-1.28.15.post1.tar.gz", hash = "sha256:c3c3923611c0e4d3ba1e6b83de19a4fc7044ba0cb35a5a1f84fb77adf114894e"}, 209 | {file = "boto3_stubs-1.28.15.post1-py3-none-any.whl", hash = "sha256:be22c05ea4d57cddc8678ebad6a4a7f04b82afe1a729b477c6122f421416d061"}, 210 | ] 211 | 212 | [package.dependencies] 213 | botocore-stubs = "*" 214 | types-s3transfer = "*" 215 | 216 | [package.extras] 217 | accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.28.0,<1.29.0)"] 218 | account = ["mypy-boto3-account (>=1.28.0,<1.29.0)"] 219 | acm = ["mypy-boto3-acm (>=1.28.0,<1.29.0)"] 220 | acm-pca = ["mypy-boto3-acm-pca (>=1.28.0,<1.29.0)"] 221 | alexaforbusiness = ["mypy-boto3-alexaforbusiness (>=1.28.0,<1.29.0)"] 222 | all = ["mypy-boto3-accessanalyzer (>=1.28.0,<1.29.0)", "mypy-boto3-account (>=1.28.0,<1.29.0)", "mypy-boto3-acm (>=1.28.0,<1.29.0)", "mypy-boto3-acm-pca (>=1.28.0,<1.29.0)", "mypy-boto3-alexaforbusiness (>=1.28.0,<1.29.0)", "mypy-boto3-amp (>=1.28.0,<1.29.0)", "mypy-boto3-amplify (>=1.28.0,<1.29.0)", "mypy-boto3-amplifybackend (>=1.28.0,<1.29.0)", "mypy-boto3-amplifyuibuilder (>=1.28.0,<1.29.0)", "mypy-boto3-apigateway (>=1.28.0,<1.29.0)", "mypy-boto3-apigatewaymanagementapi (>=1.28.0,<1.29.0)", "mypy-boto3-apigatewayv2 (>=1.28.0,<1.29.0)", "mypy-boto3-appconfig (>=1.28.0,<1.29.0)", "mypy-boto3-appconfigdata (>=1.28.0,<1.29.0)", "mypy-boto3-appfabric (>=1.28.0,<1.29.0)", "mypy-boto3-appflow (>=1.28.0,<1.29.0)", "mypy-boto3-appintegrations (>=1.28.0,<1.29.0)", "mypy-boto3-application-autoscaling (>=1.28.0,<1.29.0)", "mypy-boto3-application-insights (>=1.28.0,<1.29.0)", "mypy-boto3-applicationcostprofiler (>=1.28.0,<1.29.0)", "mypy-boto3-appmesh (>=1.28.0,<1.29.0)", "mypy-boto3-apprunner (>=1.28.0,<1.29.0)", "mypy-boto3-appstream (>=1.28.0,<1.29.0)", "mypy-boto3-appsync (>=1.28.0,<1.29.0)", "mypy-boto3-arc-zonal-shift (>=1.28.0,<1.29.0)", "mypy-boto3-athena (>=1.28.0,<1.29.0)", "mypy-boto3-auditmanager (>=1.28.0,<1.29.0)", "mypy-boto3-autoscaling (>=1.28.0,<1.29.0)", "mypy-boto3-autoscaling-plans (>=1.28.0,<1.29.0)", "mypy-boto3-backup (>=1.28.0,<1.29.0)", "mypy-boto3-backup-gateway (>=1.28.0,<1.29.0)", "mypy-boto3-backupstorage (>=1.28.0,<1.29.0)", "mypy-boto3-batch (>=1.28.0,<1.29.0)", "mypy-boto3-billingconductor (>=1.28.0,<1.29.0)", "mypy-boto3-braket (>=1.28.0,<1.29.0)", "mypy-boto3-budgets (>=1.28.0,<1.29.0)", "mypy-boto3-ce (>=1.28.0,<1.29.0)", "mypy-boto3-chime (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-identity (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-meetings (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-messaging (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-voice (>=1.28.0,<1.29.0)", "mypy-boto3-cleanrooms (>=1.28.0,<1.29.0)", "mypy-boto3-cloud9 (>=1.28.0,<1.29.0)", "mypy-boto3-cloudcontrol (>=1.28.0,<1.29.0)", "mypy-boto3-clouddirectory (>=1.28.0,<1.29.0)", "mypy-boto3-cloudformation (>=1.28.0,<1.29.0)", "mypy-boto3-cloudfront (>=1.28.0,<1.29.0)", "mypy-boto3-cloudhsm (>=1.28.0,<1.29.0)", "mypy-boto3-cloudhsmv2 (>=1.28.0,<1.29.0)", "mypy-boto3-cloudsearch (>=1.28.0,<1.29.0)", "mypy-boto3-cloudsearchdomain (>=1.28.0,<1.29.0)", "mypy-boto3-cloudtrail (>=1.28.0,<1.29.0)", "mypy-boto3-cloudtrail-data (>=1.28.0,<1.29.0)", "mypy-boto3-cloudwatch (>=1.28.0,<1.29.0)", "mypy-boto3-codeartifact (>=1.28.0,<1.29.0)", "mypy-boto3-codebuild (>=1.28.0,<1.29.0)", "mypy-boto3-codecatalyst (>=1.28.0,<1.29.0)", "mypy-boto3-codecommit (>=1.28.0,<1.29.0)", "mypy-boto3-codedeploy (>=1.28.0,<1.29.0)", "mypy-boto3-codeguru-reviewer (>=1.28.0,<1.29.0)", "mypy-boto3-codeguru-security (>=1.28.0,<1.29.0)", "mypy-boto3-codeguruprofiler (>=1.28.0,<1.29.0)", "mypy-boto3-codepipeline (>=1.28.0,<1.29.0)", "mypy-boto3-codestar (>=1.28.0,<1.29.0)", "mypy-boto3-codestar-connections (>=1.28.0,<1.29.0)", "mypy-boto3-codestar-notifications (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-identity (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-idp (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-sync (>=1.28.0,<1.29.0)", "mypy-boto3-comprehend (>=1.28.0,<1.29.0)", "mypy-boto3-comprehendmedical (>=1.28.0,<1.29.0)", "mypy-boto3-compute-optimizer (>=1.28.0,<1.29.0)", "mypy-boto3-config (>=1.28.0,<1.29.0)", "mypy-boto3-connect (>=1.28.0,<1.29.0)", "mypy-boto3-connect-contact-lens (>=1.28.0,<1.29.0)", "mypy-boto3-connectcampaigns (>=1.28.0,<1.29.0)", "mypy-boto3-connectcases (>=1.28.0,<1.29.0)", "mypy-boto3-connectparticipant (>=1.28.0,<1.29.0)", "mypy-boto3-controltower (>=1.28.0,<1.29.0)", "mypy-boto3-cur (>=1.28.0,<1.29.0)", "mypy-boto3-customer-profiles (>=1.28.0,<1.29.0)", "mypy-boto3-databrew (>=1.28.0,<1.29.0)", "mypy-boto3-dataexchange (>=1.28.0,<1.29.0)", "mypy-boto3-datapipeline (>=1.28.0,<1.29.0)", "mypy-boto3-datasync (>=1.28.0,<1.29.0)", "mypy-boto3-dax (>=1.28.0,<1.29.0)", "mypy-boto3-detective (>=1.28.0,<1.29.0)", "mypy-boto3-devicefarm (>=1.28.0,<1.29.0)", "mypy-boto3-devops-guru (>=1.28.0,<1.29.0)", "mypy-boto3-directconnect (>=1.28.0,<1.29.0)", "mypy-boto3-discovery (>=1.28.0,<1.29.0)", "mypy-boto3-dlm (>=1.28.0,<1.29.0)", "mypy-boto3-dms (>=1.28.0,<1.29.0)", "mypy-boto3-docdb (>=1.28.0,<1.29.0)", "mypy-boto3-docdb-elastic (>=1.28.0,<1.29.0)", "mypy-boto3-drs (>=1.28.0,<1.29.0)", "mypy-boto3-ds (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodb (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodbstreams (>=1.28.0,<1.29.0)", "mypy-boto3-ebs (>=1.28.0,<1.29.0)", "mypy-boto3-ec2 (>=1.28.0,<1.29.0)", "mypy-boto3-ec2-instance-connect (>=1.28.0,<1.29.0)", "mypy-boto3-ecr (>=1.28.0,<1.29.0)", "mypy-boto3-ecr-public (>=1.28.0,<1.29.0)", "mypy-boto3-ecs (>=1.28.0,<1.29.0)", "mypy-boto3-efs (>=1.28.0,<1.29.0)", "mypy-boto3-eks (>=1.28.0,<1.29.0)", "mypy-boto3-elastic-inference (>=1.28.0,<1.29.0)", "mypy-boto3-elasticache (>=1.28.0,<1.29.0)", "mypy-boto3-elasticbeanstalk (>=1.28.0,<1.29.0)", "mypy-boto3-elastictranscoder (>=1.28.0,<1.29.0)", "mypy-boto3-elb (>=1.28.0,<1.29.0)", "mypy-boto3-elbv2 (>=1.28.0,<1.29.0)", "mypy-boto3-emr (>=1.28.0,<1.29.0)", "mypy-boto3-emr-containers (>=1.28.0,<1.29.0)", "mypy-boto3-emr-serverless (>=1.28.0,<1.29.0)", "mypy-boto3-entityresolution (>=1.28.0,<1.29.0)", "mypy-boto3-es (>=1.28.0,<1.29.0)", "mypy-boto3-events (>=1.28.0,<1.29.0)", "mypy-boto3-evidently (>=1.28.0,<1.29.0)", "mypy-boto3-finspace (>=1.28.0,<1.29.0)", "mypy-boto3-finspace-data (>=1.28.0,<1.29.0)", "mypy-boto3-firehose (>=1.28.0,<1.29.0)", "mypy-boto3-fis (>=1.28.0,<1.29.0)", "mypy-boto3-fms (>=1.28.0,<1.29.0)", "mypy-boto3-forecast (>=1.28.0,<1.29.0)", "mypy-boto3-forecastquery (>=1.28.0,<1.29.0)", "mypy-boto3-frauddetector (>=1.28.0,<1.29.0)", "mypy-boto3-fsx (>=1.28.0,<1.29.0)", "mypy-boto3-gamelift (>=1.28.0,<1.29.0)", "mypy-boto3-gamesparks (>=1.28.0,<1.29.0)", "mypy-boto3-glacier (>=1.28.0,<1.29.0)", "mypy-boto3-globalaccelerator (>=1.28.0,<1.29.0)", "mypy-boto3-glue (>=1.28.0,<1.29.0)", "mypy-boto3-grafana (>=1.28.0,<1.29.0)", "mypy-boto3-greengrass (>=1.28.0,<1.29.0)", "mypy-boto3-greengrassv2 (>=1.28.0,<1.29.0)", "mypy-boto3-groundstation (>=1.28.0,<1.29.0)", "mypy-boto3-guardduty (>=1.28.0,<1.29.0)", "mypy-boto3-health (>=1.28.0,<1.29.0)", "mypy-boto3-healthlake (>=1.28.0,<1.29.0)", "mypy-boto3-honeycode (>=1.28.0,<1.29.0)", "mypy-boto3-iam (>=1.28.0,<1.29.0)", "mypy-boto3-identitystore (>=1.28.0,<1.29.0)", "mypy-boto3-imagebuilder (>=1.28.0,<1.29.0)", "mypy-boto3-importexport (>=1.28.0,<1.29.0)", "mypy-boto3-inspector (>=1.28.0,<1.29.0)", "mypy-boto3-inspector2 (>=1.28.0,<1.29.0)", "mypy-boto3-internetmonitor (>=1.28.0,<1.29.0)", "mypy-boto3-iot (>=1.28.0,<1.29.0)", "mypy-boto3-iot-data (>=1.28.0,<1.29.0)", "mypy-boto3-iot-jobs-data (>=1.28.0,<1.29.0)", "mypy-boto3-iot-roborunner (>=1.28.0,<1.29.0)", "mypy-boto3-iot1click-devices (>=1.28.0,<1.29.0)", "mypy-boto3-iot1click-projects (>=1.28.0,<1.29.0)", "mypy-boto3-iotanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-iotdeviceadvisor (>=1.28.0,<1.29.0)", "mypy-boto3-iotevents (>=1.28.0,<1.29.0)", "mypy-boto3-iotevents-data (>=1.28.0,<1.29.0)", "mypy-boto3-iotfleethub (>=1.28.0,<1.29.0)", "mypy-boto3-iotfleetwise (>=1.28.0,<1.29.0)", "mypy-boto3-iotsecuretunneling (>=1.28.0,<1.29.0)", "mypy-boto3-iotsitewise (>=1.28.0,<1.29.0)", "mypy-boto3-iotthingsgraph (>=1.28.0,<1.29.0)", "mypy-boto3-iottwinmaker (>=1.28.0,<1.29.0)", "mypy-boto3-iotwireless (>=1.28.0,<1.29.0)", "mypy-boto3-ivs (>=1.28.0,<1.29.0)", "mypy-boto3-ivs-realtime (>=1.28.0,<1.29.0)", "mypy-boto3-ivschat (>=1.28.0,<1.29.0)", "mypy-boto3-kafka (>=1.28.0,<1.29.0)", "mypy-boto3-kafkaconnect (>=1.28.0,<1.29.0)", "mypy-boto3-kendra (>=1.28.0,<1.29.0)", "mypy-boto3-kendra-ranking (>=1.28.0,<1.29.0)", "mypy-boto3-keyspaces (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-archived-media (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-media (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-signaling (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisvideo (>=1.28.0,<1.29.0)", "mypy-boto3-kms (>=1.28.0,<1.29.0)", "mypy-boto3-lakeformation (>=1.28.0,<1.29.0)", "mypy-boto3-lambda (>=1.28.0,<1.29.0)", "mypy-boto3-lex-models (>=1.28.0,<1.29.0)", "mypy-boto3-lex-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-lexv2-models (>=1.28.0,<1.29.0)", "mypy-boto3-lexv2-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.28.0,<1.29.0)", "mypy-boto3-lightsail (>=1.28.0,<1.29.0)", "mypy-boto3-location (>=1.28.0,<1.29.0)", "mypy-boto3-logs (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutequipment (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutmetrics (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutvision (>=1.28.0,<1.29.0)", "mypy-boto3-m2 (>=1.28.0,<1.29.0)", "mypy-boto3-machinelearning (>=1.28.0,<1.29.0)", "mypy-boto3-macie (>=1.28.0,<1.29.0)", "mypy-boto3-macie2 (>=1.28.0,<1.29.0)", "mypy-boto3-managedblockchain (>=1.28.0,<1.29.0)", "mypy-boto3-managedblockchain-query (>=1.28.0,<1.29.0)", "mypy-boto3-marketplace-catalog (>=1.28.0,<1.29.0)", "mypy-boto3-marketplace-entitlement (>=1.28.0,<1.29.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-mediaconnect (>=1.28.0,<1.29.0)", "mypy-boto3-mediaconvert (>=1.28.0,<1.29.0)", "mypy-boto3-medialive (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackage (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackage-vod (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackagev2 (>=1.28.0,<1.29.0)", "mypy-boto3-mediastore (>=1.28.0,<1.29.0)", "mypy-boto3-mediastore-data (>=1.28.0,<1.29.0)", "mypy-boto3-mediatailor (>=1.28.0,<1.29.0)", "mypy-boto3-medical-imaging (>=1.28.0,<1.29.0)", "mypy-boto3-memorydb (>=1.28.0,<1.29.0)", "mypy-boto3-meteringmarketplace (>=1.28.0,<1.29.0)", "mypy-boto3-mgh (>=1.28.0,<1.29.0)", "mypy-boto3-mgn (>=1.28.0,<1.29.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhub-config (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhuborchestrator (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhubstrategy (>=1.28.0,<1.29.0)", "mypy-boto3-mobile (>=1.28.0,<1.29.0)", "mypy-boto3-mq (>=1.28.0,<1.29.0)", "mypy-boto3-mturk (>=1.28.0,<1.29.0)", "mypy-boto3-mwaa (>=1.28.0,<1.29.0)", "mypy-boto3-neptune (>=1.28.0,<1.29.0)", "mypy-boto3-network-firewall (>=1.28.0,<1.29.0)", "mypy-boto3-networkmanager (>=1.28.0,<1.29.0)", "mypy-boto3-nimble (>=1.28.0,<1.29.0)", "mypy-boto3-oam (>=1.28.0,<1.29.0)", "mypy-boto3-omics (>=1.28.0,<1.29.0)", "mypy-boto3-opensearch (>=1.28.0,<1.29.0)", "mypy-boto3-opensearchserverless (>=1.28.0,<1.29.0)", "mypy-boto3-opsworks (>=1.28.0,<1.29.0)", "mypy-boto3-opsworkscm (>=1.28.0,<1.29.0)", "mypy-boto3-organizations (>=1.28.0,<1.29.0)", "mypy-boto3-osis (>=1.28.0,<1.29.0)", "mypy-boto3-outposts (>=1.28.0,<1.29.0)", "mypy-boto3-panorama (>=1.28.0,<1.29.0)", "mypy-boto3-payment-cryptography (>=1.28.0,<1.29.0)", "mypy-boto3-payment-cryptography-data (>=1.28.0,<1.29.0)", "mypy-boto3-personalize (>=1.28.0,<1.29.0)", "mypy-boto3-personalize-events (>=1.28.0,<1.29.0)", "mypy-boto3-personalize-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-pi (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-email (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-sms-voice (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.28.0,<1.29.0)", "mypy-boto3-pipes (>=1.28.0,<1.29.0)", "mypy-boto3-polly (>=1.28.0,<1.29.0)", "mypy-boto3-pricing (>=1.28.0,<1.29.0)", "mypy-boto3-privatenetworks (>=1.28.0,<1.29.0)", "mypy-boto3-proton (>=1.28.0,<1.29.0)", "mypy-boto3-qldb (>=1.28.0,<1.29.0)", "mypy-boto3-qldb-session (>=1.28.0,<1.29.0)", "mypy-boto3-quicksight (>=1.28.0,<1.29.0)", "mypy-boto3-ram (>=1.28.0,<1.29.0)", "mypy-boto3-rbin (>=1.28.0,<1.29.0)", "mypy-boto3-rds (>=1.28.0,<1.29.0)", "mypy-boto3-rds-data (>=1.28.0,<1.29.0)", "mypy-boto3-redshift (>=1.28.0,<1.29.0)", "mypy-boto3-redshift-data (>=1.28.0,<1.29.0)", "mypy-boto3-redshift-serverless (>=1.28.0,<1.29.0)", "mypy-boto3-rekognition (>=1.28.0,<1.29.0)", "mypy-boto3-resiliencehub (>=1.28.0,<1.29.0)", "mypy-boto3-resource-explorer-2 (>=1.28.0,<1.29.0)", "mypy-boto3-resource-groups (>=1.28.0,<1.29.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.28.0,<1.29.0)", "mypy-boto3-robomaker (>=1.28.0,<1.29.0)", "mypy-boto3-rolesanywhere (>=1.28.0,<1.29.0)", "mypy-boto3-route53 (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-cluster (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-control-config (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-readiness (>=1.28.0,<1.29.0)", "mypy-boto3-route53domains (>=1.28.0,<1.29.0)", "mypy-boto3-route53resolver (>=1.28.0,<1.29.0)", "mypy-boto3-rum (>=1.28.0,<1.29.0)", "mypy-boto3-s3 (>=1.28.0,<1.29.0)", "mypy-boto3-s3control (>=1.28.0,<1.29.0)", "mypy-boto3-s3outposts (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-edge (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-geospatial (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-metrics (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-savingsplans (>=1.28.0,<1.29.0)", "mypy-boto3-scheduler (>=1.28.0,<1.29.0)", "mypy-boto3-schemas (>=1.28.0,<1.29.0)", "mypy-boto3-sdb (>=1.28.0,<1.29.0)", "mypy-boto3-secretsmanager (>=1.28.0,<1.29.0)", "mypy-boto3-securityhub (>=1.28.0,<1.29.0)", "mypy-boto3-securitylake (>=1.28.0,<1.29.0)", "mypy-boto3-serverlessrepo (>=1.28.0,<1.29.0)", "mypy-boto3-service-quotas (>=1.28.0,<1.29.0)", "mypy-boto3-servicecatalog (>=1.28.0,<1.29.0)", "mypy-boto3-servicecatalog-appregistry (>=1.28.0,<1.29.0)", "mypy-boto3-servicediscovery (>=1.28.0,<1.29.0)", "mypy-boto3-ses (>=1.28.0,<1.29.0)", "mypy-boto3-sesv2 (>=1.28.0,<1.29.0)", "mypy-boto3-shield (>=1.28.0,<1.29.0)", "mypy-boto3-signer (>=1.28.0,<1.29.0)", "mypy-boto3-simspaceweaver (>=1.28.0,<1.29.0)", "mypy-boto3-sms (>=1.28.0,<1.29.0)", "mypy-boto3-sms-voice (>=1.28.0,<1.29.0)", "mypy-boto3-snow-device-management (>=1.28.0,<1.29.0)", "mypy-boto3-snowball (>=1.28.0,<1.29.0)", "mypy-boto3-sns (>=1.28.0,<1.29.0)", "mypy-boto3-sqs (>=1.28.0,<1.29.0)", "mypy-boto3-ssm (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-contacts (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-incidents (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-sap (>=1.28.0,<1.29.0)", "mypy-boto3-sso (>=1.28.0,<1.29.0)", "mypy-boto3-sso-admin (>=1.28.0,<1.29.0)", "mypy-boto3-sso-oidc (>=1.28.0,<1.29.0)", "mypy-boto3-stepfunctions (>=1.28.0,<1.29.0)", "mypy-boto3-storagegateway (>=1.28.0,<1.29.0)", "mypy-boto3-sts (>=1.28.0,<1.29.0)", "mypy-boto3-support (>=1.28.0,<1.29.0)", "mypy-boto3-support-app (>=1.28.0,<1.29.0)", "mypy-boto3-swf (>=1.28.0,<1.29.0)", "mypy-boto3-synthetics (>=1.28.0,<1.29.0)", "mypy-boto3-textract (>=1.28.0,<1.29.0)", "mypy-boto3-timestream-query (>=1.28.0,<1.29.0)", "mypy-boto3-timestream-write (>=1.28.0,<1.29.0)", "mypy-boto3-tnb (>=1.28.0,<1.29.0)", "mypy-boto3-transcribe (>=1.28.0,<1.29.0)", "mypy-boto3-transfer (>=1.28.0,<1.29.0)", "mypy-boto3-translate (>=1.28.0,<1.29.0)", "mypy-boto3-verifiedpermissions (>=1.28.0,<1.29.0)", "mypy-boto3-voice-id (>=1.28.0,<1.29.0)", "mypy-boto3-vpc-lattice (>=1.28.0,<1.29.0)", "mypy-boto3-waf (>=1.28.0,<1.29.0)", "mypy-boto3-waf-regional (>=1.28.0,<1.29.0)", "mypy-boto3-wafv2 (>=1.28.0,<1.29.0)", "mypy-boto3-wellarchitected (>=1.28.0,<1.29.0)", "mypy-boto3-wisdom (>=1.28.0,<1.29.0)", "mypy-boto3-workdocs (>=1.28.0,<1.29.0)", "mypy-boto3-worklink (>=1.28.0,<1.29.0)", "mypy-boto3-workmail (>=1.28.0,<1.29.0)", "mypy-boto3-workmailmessageflow (>=1.28.0,<1.29.0)", "mypy-boto3-workspaces (>=1.28.0,<1.29.0)", "mypy-boto3-workspaces-web (>=1.28.0,<1.29.0)", "mypy-boto3-xray (>=1.28.0,<1.29.0)"] 223 | amp = ["mypy-boto3-amp (>=1.28.0,<1.29.0)"] 224 | amplify = ["mypy-boto3-amplify (>=1.28.0,<1.29.0)"] 225 | amplifybackend = ["mypy-boto3-amplifybackend (>=1.28.0,<1.29.0)"] 226 | amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.28.0,<1.29.0)"] 227 | apigateway = ["mypy-boto3-apigateway (>=1.28.0,<1.29.0)"] 228 | apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.28.0,<1.29.0)"] 229 | apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.28.0,<1.29.0)"] 230 | appconfig = ["mypy-boto3-appconfig (>=1.28.0,<1.29.0)"] 231 | appconfigdata = ["mypy-boto3-appconfigdata (>=1.28.0,<1.29.0)"] 232 | appfabric = ["mypy-boto3-appfabric (>=1.28.0,<1.29.0)"] 233 | appflow = ["mypy-boto3-appflow (>=1.28.0,<1.29.0)"] 234 | appintegrations = ["mypy-boto3-appintegrations (>=1.28.0,<1.29.0)"] 235 | application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.28.0,<1.29.0)"] 236 | application-insights = ["mypy-boto3-application-insights (>=1.28.0,<1.29.0)"] 237 | applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.28.0,<1.29.0)"] 238 | appmesh = ["mypy-boto3-appmesh (>=1.28.0,<1.29.0)"] 239 | apprunner = ["mypy-boto3-apprunner (>=1.28.0,<1.29.0)"] 240 | appstream = ["mypy-boto3-appstream (>=1.28.0,<1.29.0)"] 241 | appsync = ["mypy-boto3-appsync (>=1.28.0,<1.29.0)"] 242 | arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.28.0,<1.29.0)"] 243 | athena = ["mypy-boto3-athena (>=1.28.0,<1.29.0)"] 244 | auditmanager = ["mypy-boto3-auditmanager (>=1.28.0,<1.29.0)"] 245 | autoscaling = ["mypy-boto3-autoscaling (>=1.28.0,<1.29.0)"] 246 | autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.28.0,<1.29.0)"] 247 | backup = ["mypy-boto3-backup (>=1.28.0,<1.29.0)"] 248 | backup-gateway = ["mypy-boto3-backup-gateway (>=1.28.0,<1.29.0)"] 249 | backupstorage = ["mypy-boto3-backupstorage (>=1.28.0,<1.29.0)"] 250 | batch = ["mypy-boto3-batch (>=1.28.0,<1.29.0)"] 251 | billingconductor = ["mypy-boto3-billingconductor (>=1.28.0,<1.29.0)"] 252 | boto3 = ["boto3 (==1.28.15)", "botocore (==1.31.15)"] 253 | braket = ["mypy-boto3-braket (>=1.28.0,<1.29.0)"] 254 | budgets = ["mypy-boto3-budgets (>=1.28.0,<1.29.0)"] 255 | ce = ["mypy-boto3-ce (>=1.28.0,<1.29.0)"] 256 | chime = ["mypy-boto3-chime (>=1.28.0,<1.29.0)"] 257 | chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.28.0,<1.29.0)"] 258 | chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.28.0,<1.29.0)"] 259 | chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.28.0,<1.29.0)"] 260 | chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.28.0,<1.29.0)"] 261 | chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.28.0,<1.29.0)"] 262 | cleanrooms = ["mypy-boto3-cleanrooms (>=1.28.0,<1.29.0)"] 263 | cloud9 = ["mypy-boto3-cloud9 (>=1.28.0,<1.29.0)"] 264 | cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.28.0,<1.29.0)"] 265 | clouddirectory = ["mypy-boto3-clouddirectory (>=1.28.0,<1.29.0)"] 266 | cloudformation = ["mypy-boto3-cloudformation (>=1.28.0,<1.29.0)"] 267 | cloudfront = ["mypy-boto3-cloudfront (>=1.28.0,<1.29.0)"] 268 | cloudhsm = ["mypy-boto3-cloudhsm (>=1.28.0,<1.29.0)"] 269 | cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.28.0,<1.29.0)"] 270 | cloudsearch = ["mypy-boto3-cloudsearch (>=1.28.0,<1.29.0)"] 271 | cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.28.0,<1.29.0)"] 272 | cloudtrail = ["mypy-boto3-cloudtrail (>=1.28.0,<1.29.0)"] 273 | cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.28.0,<1.29.0)"] 274 | cloudwatch = ["mypy-boto3-cloudwatch (>=1.28.0,<1.29.0)"] 275 | codeartifact = ["mypy-boto3-codeartifact (>=1.28.0,<1.29.0)"] 276 | codebuild = ["mypy-boto3-codebuild (>=1.28.0,<1.29.0)"] 277 | codecatalyst = ["mypy-boto3-codecatalyst (>=1.28.0,<1.29.0)"] 278 | codecommit = ["mypy-boto3-codecommit (>=1.28.0,<1.29.0)"] 279 | codedeploy = ["mypy-boto3-codedeploy (>=1.28.0,<1.29.0)"] 280 | codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.28.0,<1.29.0)"] 281 | codeguru-security = ["mypy-boto3-codeguru-security (>=1.28.0,<1.29.0)"] 282 | codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.28.0,<1.29.0)"] 283 | codepipeline = ["mypy-boto3-codepipeline (>=1.28.0,<1.29.0)"] 284 | codestar = ["mypy-boto3-codestar (>=1.28.0,<1.29.0)"] 285 | codestar-connections = ["mypy-boto3-codestar-connections (>=1.28.0,<1.29.0)"] 286 | codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.28.0,<1.29.0)"] 287 | cognito-identity = ["mypy-boto3-cognito-identity (>=1.28.0,<1.29.0)"] 288 | cognito-idp = ["mypy-boto3-cognito-idp (>=1.28.0,<1.29.0)"] 289 | cognito-sync = ["mypy-boto3-cognito-sync (>=1.28.0,<1.29.0)"] 290 | comprehend = ["mypy-boto3-comprehend (>=1.28.0,<1.29.0)"] 291 | comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.28.0,<1.29.0)"] 292 | compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.28.0,<1.29.0)"] 293 | config = ["mypy-boto3-config (>=1.28.0,<1.29.0)"] 294 | connect = ["mypy-boto3-connect (>=1.28.0,<1.29.0)"] 295 | connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.28.0,<1.29.0)"] 296 | connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.28.0,<1.29.0)"] 297 | connectcases = ["mypy-boto3-connectcases (>=1.28.0,<1.29.0)"] 298 | connectparticipant = ["mypy-boto3-connectparticipant (>=1.28.0,<1.29.0)"] 299 | controltower = ["mypy-boto3-controltower (>=1.28.0,<1.29.0)"] 300 | cur = ["mypy-boto3-cur (>=1.28.0,<1.29.0)"] 301 | customer-profiles = ["mypy-boto3-customer-profiles (>=1.28.0,<1.29.0)"] 302 | databrew = ["mypy-boto3-databrew (>=1.28.0,<1.29.0)"] 303 | dataexchange = ["mypy-boto3-dataexchange (>=1.28.0,<1.29.0)"] 304 | datapipeline = ["mypy-boto3-datapipeline (>=1.28.0,<1.29.0)"] 305 | datasync = ["mypy-boto3-datasync (>=1.28.0,<1.29.0)"] 306 | dax = ["mypy-boto3-dax (>=1.28.0,<1.29.0)"] 307 | detective = ["mypy-boto3-detective (>=1.28.0,<1.29.0)"] 308 | devicefarm = ["mypy-boto3-devicefarm (>=1.28.0,<1.29.0)"] 309 | devops-guru = ["mypy-boto3-devops-guru (>=1.28.0,<1.29.0)"] 310 | directconnect = ["mypy-boto3-directconnect (>=1.28.0,<1.29.0)"] 311 | discovery = ["mypy-boto3-discovery (>=1.28.0,<1.29.0)"] 312 | dlm = ["mypy-boto3-dlm (>=1.28.0,<1.29.0)"] 313 | dms = ["mypy-boto3-dms (>=1.28.0,<1.29.0)"] 314 | docdb = ["mypy-boto3-docdb (>=1.28.0,<1.29.0)"] 315 | docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.28.0,<1.29.0)"] 316 | drs = ["mypy-boto3-drs (>=1.28.0,<1.29.0)"] 317 | ds = ["mypy-boto3-ds (>=1.28.0,<1.29.0)"] 318 | dynamodb = ["mypy-boto3-dynamodb (>=1.28.0,<1.29.0)"] 319 | dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.28.0,<1.29.0)"] 320 | ebs = ["mypy-boto3-ebs (>=1.28.0,<1.29.0)"] 321 | ec2 = ["mypy-boto3-ec2 (>=1.28.0,<1.29.0)"] 322 | ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.28.0,<1.29.0)"] 323 | ecr = ["mypy-boto3-ecr (>=1.28.0,<1.29.0)"] 324 | ecr-public = ["mypy-boto3-ecr-public (>=1.28.0,<1.29.0)"] 325 | ecs = ["mypy-boto3-ecs (>=1.28.0,<1.29.0)"] 326 | efs = ["mypy-boto3-efs (>=1.28.0,<1.29.0)"] 327 | eks = ["mypy-boto3-eks (>=1.28.0,<1.29.0)"] 328 | elastic-inference = ["mypy-boto3-elastic-inference (>=1.28.0,<1.29.0)"] 329 | elasticache = ["mypy-boto3-elasticache (>=1.28.0,<1.29.0)"] 330 | elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.28.0,<1.29.0)"] 331 | elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.28.0,<1.29.0)"] 332 | elb = ["mypy-boto3-elb (>=1.28.0,<1.29.0)"] 333 | elbv2 = ["mypy-boto3-elbv2 (>=1.28.0,<1.29.0)"] 334 | emr = ["mypy-boto3-emr (>=1.28.0,<1.29.0)"] 335 | emr-containers = ["mypy-boto3-emr-containers (>=1.28.0,<1.29.0)"] 336 | emr-serverless = ["mypy-boto3-emr-serverless (>=1.28.0,<1.29.0)"] 337 | entityresolution = ["mypy-boto3-entityresolution (>=1.28.0,<1.29.0)"] 338 | es = ["mypy-boto3-es (>=1.28.0,<1.29.0)"] 339 | essential = ["mypy-boto3-cloudformation (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodb (>=1.28.0,<1.29.0)", "mypy-boto3-ec2 (>=1.28.0,<1.29.0)", "mypy-boto3-lambda (>=1.28.0,<1.29.0)", "mypy-boto3-rds (>=1.28.0,<1.29.0)", "mypy-boto3-s3 (>=1.28.0,<1.29.0)", "mypy-boto3-sqs (>=1.28.0,<1.29.0)"] 340 | events = ["mypy-boto3-events (>=1.28.0,<1.29.0)"] 341 | evidently = ["mypy-boto3-evidently (>=1.28.0,<1.29.0)"] 342 | finspace = ["mypy-boto3-finspace (>=1.28.0,<1.29.0)"] 343 | finspace-data = ["mypy-boto3-finspace-data (>=1.28.0,<1.29.0)"] 344 | firehose = ["mypy-boto3-firehose (>=1.28.0,<1.29.0)"] 345 | fis = ["mypy-boto3-fis (>=1.28.0,<1.29.0)"] 346 | fms = ["mypy-boto3-fms (>=1.28.0,<1.29.0)"] 347 | forecast = ["mypy-boto3-forecast (>=1.28.0,<1.29.0)"] 348 | forecastquery = ["mypy-boto3-forecastquery (>=1.28.0,<1.29.0)"] 349 | frauddetector = ["mypy-boto3-frauddetector (>=1.28.0,<1.29.0)"] 350 | fsx = ["mypy-boto3-fsx (>=1.28.0,<1.29.0)"] 351 | gamelift = ["mypy-boto3-gamelift (>=1.28.0,<1.29.0)"] 352 | gamesparks = ["mypy-boto3-gamesparks (>=1.28.0,<1.29.0)"] 353 | glacier = ["mypy-boto3-glacier (>=1.28.0,<1.29.0)"] 354 | globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.28.0,<1.29.0)"] 355 | glue = ["mypy-boto3-glue (>=1.28.0,<1.29.0)"] 356 | grafana = ["mypy-boto3-grafana (>=1.28.0,<1.29.0)"] 357 | greengrass = ["mypy-boto3-greengrass (>=1.28.0,<1.29.0)"] 358 | greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.28.0,<1.29.0)"] 359 | groundstation = ["mypy-boto3-groundstation (>=1.28.0,<1.29.0)"] 360 | guardduty = ["mypy-boto3-guardduty (>=1.28.0,<1.29.0)"] 361 | health = ["mypy-boto3-health (>=1.28.0,<1.29.0)"] 362 | healthlake = ["mypy-boto3-healthlake (>=1.28.0,<1.29.0)"] 363 | honeycode = ["mypy-boto3-honeycode (>=1.28.0,<1.29.0)"] 364 | iam = ["mypy-boto3-iam (>=1.28.0,<1.29.0)"] 365 | identitystore = ["mypy-boto3-identitystore (>=1.28.0,<1.29.0)"] 366 | imagebuilder = ["mypy-boto3-imagebuilder (>=1.28.0,<1.29.0)"] 367 | importexport = ["mypy-boto3-importexport (>=1.28.0,<1.29.0)"] 368 | inspector = ["mypy-boto3-inspector (>=1.28.0,<1.29.0)"] 369 | inspector2 = ["mypy-boto3-inspector2 (>=1.28.0,<1.29.0)"] 370 | internetmonitor = ["mypy-boto3-internetmonitor (>=1.28.0,<1.29.0)"] 371 | iot = ["mypy-boto3-iot (>=1.28.0,<1.29.0)"] 372 | iot-data = ["mypy-boto3-iot-data (>=1.28.0,<1.29.0)"] 373 | iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.28.0,<1.29.0)"] 374 | iot-roborunner = ["mypy-boto3-iot-roborunner (>=1.28.0,<1.29.0)"] 375 | iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.28.0,<1.29.0)"] 376 | iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.28.0,<1.29.0)"] 377 | iotanalytics = ["mypy-boto3-iotanalytics (>=1.28.0,<1.29.0)"] 378 | iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.28.0,<1.29.0)"] 379 | iotevents = ["mypy-boto3-iotevents (>=1.28.0,<1.29.0)"] 380 | iotevents-data = ["mypy-boto3-iotevents-data (>=1.28.0,<1.29.0)"] 381 | iotfleethub = ["mypy-boto3-iotfleethub (>=1.28.0,<1.29.0)"] 382 | iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.28.0,<1.29.0)"] 383 | iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.28.0,<1.29.0)"] 384 | iotsitewise = ["mypy-boto3-iotsitewise (>=1.28.0,<1.29.0)"] 385 | iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.28.0,<1.29.0)"] 386 | iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.28.0,<1.29.0)"] 387 | iotwireless = ["mypy-boto3-iotwireless (>=1.28.0,<1.29.0)"] 388 | ivs = ["mypy-boto3-ivs (>=1.28.0,<1.29.0)"] 389 | ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.28.0,<1.29.0)"] 390 | ivschat = ["mypy-boto3-ivschat (>=1.28.0,<1.29.0)"] 391 | kafka = ["mypy-boto3-kafka (>=1.28.0,<1.29.0)"] 392 | kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.28.0,<1.29.0)"] 393 | kendra = ["mypy-boto3-kendra (>=1.28.0,<1.29.0)"] 394 | kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.28.0,<1.29.0)"] 395 | keyspaces = ["mypy-boto3-keyspaces (>=1.28.0,<1.29.0)"] 396 | kinesis = ["mypy-boto3-kinesis (>=1.28.0,<1.29.0)"] 397 | kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.28.0,<1.29.0)"] 398 | kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.28.0,<1.29.0)"] 399 | kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.28.0,<1.29.0)"] 400 | kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.28.0,<1.29.0)"] 401 | kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.28.0,<1.29.0)"] 402 | kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.28.0,<1.29.0)"] 403 | kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.28.0,<1.29.0)"] 404 | kms = ["mypy-boto3-kms (>=1.28.0,<1.29.0)"] 405 | lakeformation = ["mypy-boto3-lakeformation (>=1.28.0,<1.29.0)"] 406 | lambda = ["mypy-boto3-lambda (>=1.28.0,<1.29.0)"] 407 | lex-models = ["mypy-boto3-lex-models (>=1.28.0,<1.29.0)"] 408 | lex-runtime = ["mypy-boto3-lex-runtime (>=1.28.0,<1.29.0)"] 409 | lexv2-models = ["mypy-boto3-lexv2-models (>=1.28.0,<1.29.0)"] 410 | lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.28.0,<1.29.0)"] 411 | license-manager = ["mypy-boto3-license-manager (>=1.28.0,<1.29.0)"] 412 | license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.28.0,<1.29.0)"] 413 | license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.28.0,<1.29.0)"] 414 | lightsail = ["mypy-boto3-lightsail (>=1.28.0,<1.29.0)"] 415 | location = ["mypy-boto3-location (>=1.28.0,<1.29.0)"] 416 | logs = ["mypy-boto3-logs (>=1.28.0,<1.29.0)"] 417 | lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.28.0,<1.29.0)"] 418 | lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.28.0,<1.29.0)"] 419 | lookoutvision = ["mypy-boto3-lookoutvision (>=1.28.0,<1.29.0)"] 420 | m2 = ["mypy-boto3-m2 (>=1.28.0,<1.29.0)"] 421 | machinelearning = ["mypy-boto3-machinelearning (>=1.28.0,<1.29.0)"] 422 | macie = ["mypy-boto3-macie (>=1.28.0,<1.29.0)"] 423 | macie2 = ["mypy-boto3-macie2 (>=1.28.0,<1.29.0)"] 424 | managedblockchain = ["mypy-boto3-managedblockchain (>=1.28.0,<1.29.0)"] 425 | managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.28.0,<1.29.0)"] 426 | marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.28.0,<1.29.0)"] 427 | marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.28.0,<1.29.0)"] 428 | marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.28.0,<1.29.0)"] 429 | mediaconnect = ["mypy-boto3-mediaconnect (>=1.28.0,<1.29.0)"] 430 | mediaconvert = ["mypy-boto3-mediaconvert (>=1.28.0,<1.29.0)"] 431 | medialive = ["mypy-boto3-medialive (>=1.28.0,<1.29.0)"] 432 | mediapackage = ["mypy-boto3-mediapackage (>=1.28.0,<1.29.0)"] 433 | mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.28.0,<1.29.0)"] 434 | mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.28.0,<1.29.0)"] 435 | mediastore = ["mypy-boto3-mediastore (>=1.28.0,<1.29.0)"] 436 | mediastore-data = ["mypy-boto3-mediastore-data (>=1.28.0,<1.29.0)"] 437 | mediatailor = ["mypy-boto3-mediatailor (>=1.28.0,<1.29.0)"] 438 | medical-imaging = ["mypy-boto3-medical-imaging (>=1.28.0,<1.29.0)"] 439 | memorydb = ["mypy-boto3-memorydb (>=1.28.0,<1.29.0)"] 440 | meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.28.0,<1.29.0)"] 441 | mgh = ["mypy-boto3-mgh (>=1.28.0,<1.29.0)"] 442 | mgn = ["mypy-boto3-mgn (>=1.28.0,<1.29.0)"] 443 | migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.28.0,<1.29.0)"] 444 | migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.28.0,<1.29.0)"] 445 | migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.28.0,<1.29.0)"] 446 | migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.28.0,<1.29.0)"] 447 | mobile = ["mypy-boto3-mobile (>=1.28.0,<1.29.0)"] 448 | mq = ["mypy-boto3-mq (>=1.28.0,<1.29.0)"] 449 | mturk = ["mypy-boto3-mturk (>=1.28.0,<1.29.0)"] 450 | mwaa = ["mypy-boto3-mwaa (>=1.28.0,<1.29.0)"] 451 | neptune = ["mypy-boto3-neptune (>=1.28.0,<1.29.0)"] 452 | network-firewall = ["mypy-boto3-network-firewall (>=1.28.0,<1.29.0)"] 453 | networkmanager = ["mypy-boto3-networkmanager (>=1.28.0,<1.29.0)"] 454 | nimble = ["mypy-boto3-nimble (>=1.28.0,<1.29.0)"] 455 | oam = ["mypy-boto3-oam (>=1.28.0,<1.29.0)"] 456 | omics = ["mypy-boto3-omics (>=1.28.0,<1.29.0)"] 457 | opensearch = ["mypy-boto3-opensearch (>=1.28.0,<1.29.0)"] 458 | opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.28.0,<1.29.0)"] 459 | opsworks = ["mypy-boto3-opsworks (>=1.28.0,<1.29.0)"] 460 | opsworkscm = ["mypy-boto3-opsworkscm (>=1.28.0,<1.29.0)"] 461 | organizations = ["mypy-boto3-organizations (>=1.28.0,<1.29.0)"] 462 | osis = ["mypy-boto3-osis (>=1.28.0,<1.29.0)"] 463 | outposts = ["mypy-boto3-outposts (>=1.28.0,<1.29.0)"] 464 | panorama = ["mypy-boto3-panorama (>=1.28.0,<1.29.0)"] 465 | payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.28.0,<1.29.0)"] 466 | payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.28.0,<1.29.0)"] 467 | personalize = ["mypy-boto3-personalize (>=1.28.0,<1.29.0)"] 468 | personalize-events = ["mypy-boto3-personalize-events (>=1.28.0,<1.29.0)"] 469 | personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.28.0,<1.29.0)"] 470 | pi = ["mypy-boto3-pi (>=1.28.0,<1.29.0)"] 471 | pinpoint = ["mypy-boto3-pinpoint (>=1.28.0,<1.29.0)"] 472 | pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.28.0,<1.29.0)"] 473 | pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.28.0,<1.29.0)"] 474 | pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.28.0,<1.29.0)"] 475 | pipes = ["mypy-boto3-pipes (>=1.28.0,<1.29.0)"] 476 | polly = ["mypy-boto3-polly (>=1.28.0,<1.29.0)"] 477 | pricing = ["mypy-boto3-pricing (>=1.28.0,<1.29.0)"] 478 | privatenetworks = ["mypy-boto3-privatenetworks (>=1.28.0,<1.29.0)"] 479 | proton = ["mypy-boto3-proton (>=1.28.0,<1.29.0)"] 480 | qldb = ["mypy-boto3-qldb (>=1.28.0,<1.29.0)"] 481 | qldb-session = ["mypy-boto3-qldb-session (>=1.28.0,<1.29.0)"] 482 | quicksight = ["mypy-boto3-quicksight (>=1.28.0,<1.29.0)"] 483 | ram = ["mypy-boto3-ram (>=1.28.0,<1.29.0)"] 484 | rbin = ["mypy-boto3-rbin (>=1.28.0,<1.29.0)"] 485 | rds = ["mypy-boto3-rds (>=1.28.0,<1.29.0)"] 486 | rds-data = ["mypy-boto3-rds-data (>=1.28.0,<1.29.0)"] 487 | redshift = ["mypy-boto3-redshift (>=1.28.0,<1.29.0)"] 488 | redshift-data = ["mypy-boto3-redshift-data (>=1.28.0,<1.29.0)"] 489 | redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.28.0,<1.29.0)"] 490 | rekognition = ["mypy-boto3-rekognition (>=1.28.0,<1.29.0)"] 491 | resiliencehub = ["mypy-boto3-resiliencehub (>=1.28.0,<1.29.0)"] 492 | resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.28.0,<1.29.0)"] 493 | resource-groups = ["mypy-boto3-resource-groups (>=1.28.0,<1.29.0)"] 494 | resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.28.0,<1.29.0)"] 495 | robomaker = ["mypy-boto3-robomaker (>=1.28.0,<1.29.0)"] 496 | rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.28.0,<1.29.0)"] 497 | route53 = ["mypy-boto3-route53 (>=1.28.0,<1.29.0)"] 498 | route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.28.0,<1.29.0)"] 499 | route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.28.0,<1.29.0)"] 500 | route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.28.0,<1.29.0)"] 501 | route53domains = ["mypy-boto3-route53domains (>=1.28.0,<1.29.0)"] 502 | route53resolver = ["mypy-boto3-route53resolver (>=1.28.0,<1.29.0)"] 503 | rum = ["mypy-boto3-rum (>=1.28.0,<1.29.0)"] 504 | s3 = ["mypy-boto3-s3 (>=1.28.0,<1.29.0)"] 505 | s3control = ["mypy-boto3-s3control (>=1.28.0,<1.29.0)"] 506 | s3outposts = ["mypy-boto3-s3outposts (>=1.28.0,<1.29.0)"] 507 | sagemaker = ["mypy-boto3-sagemaker (>=1.28.0,<1.29.0)"] 508 | sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.28.0,<1.29.0)"] 509 | sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.28.0,<1.29.0)"] 510 | sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.28.0,<1.29.0)"] 511 | sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.28.0,<1.29.0)"] 512 | sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.28.0,<1.29.0)"] 513 | sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.28.0,<1.29.0)"] 514 | savingsplans = ["mypy-boto3-savingsplans (>=1.28.0,<1.29.0)"] 515 | scheduler = ["mypy-boto3-scheduler (>=1.28.0,<1.29.0)"] 516 | schemas = ["mypy-boto3-schemas (>=1.28.0,<1.29.0)"] 517 | sdb = ["mypy-boto3-sdb (>=1.28.0,<1.29.0)"] 518 | secretsmanager = ["mypy-boto3-secretsmanager (>=1.28.0,<1.29.0)"] 519 | securityhub = ["mypy-boto3-securityhub (>=1.28.0,<1.29.0)"] 520 | securitylake = ["mypy-boto3-securitylake (>=1.28.0,<1.29.0)"] 521 | serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.28.0,<1.29.0)"] 522 | service-quotas = ["mypy-boto3-service-quotas (>=1.28.0,<1.29.0)"] 523 | servicecatalog = ["mypy-boto3-servicecatalog (>=1.28.0,<1.29.0)"] 524 | servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.28.0,<1.29.0)"] 525 | servicediscovery = ["mypy-boto3-servicediscovery (>=1.28.0,<1.29.0)"] 526 | ses = ["mypy-boto3-ses (>=1.28.0,<1.29.0)"] 527 | sesv2 = ["mypy-boto3-sesv2 (>=1.28.0,<1.29.0)"] 528 | shield = ["mypy-boto3-shield (>=1.28.0,<1.29.0)"] 529 | signer = ["mypy-boto3-signer (>=1.28.0,<1.29.0)"] 530 | simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.28.0,<1.29.0)"] 531 | sms = ["mypy-boto3-sms (>=1.28.0,<1.29.0)"] 532 | sms-voice = ["mypy-boto3-sms-voice (>=1.28.0,<1.29.0)"] 533 | snow-device-management = ["mypy-boto3-snow-device-management (>=1.28.0,<1.29.0)"] 534 | snowball = ["mypy-boto3-snowball (>=1.28.0,<1.29.0)"] 535 | sns = ["mypy-boto3-sns (>=1.28.0,<1.29.0)"] 536 | sqs = ["mypy-boto3-sqs (>=1.28.0,<1.29.0)"] 537 | ssm = ["mypy-boto3-ssm (>=1.28.0,<1.29.0)"] 538 | ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.28.0,<1.29.0)"] 539 | ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.28.0,<1.29.0)"] 540 | ssm-sap = ["mypy-boto3-ssm-sap (>=1.28.0,<1.29.0)"] 541 | sso = ["mypy-boto3-sso (>=1.28.0,<1.29.0)"] 542 | sso-admin = ["mypy-boto3-sso-admin (>=1.28.0,<1.29.0)"] 543 | sso-oidc = ["mypy-boto3-sso-oidc (>=1.28.0,<1.29.0)"] 544 | stepfunctions = ["mypy-boto3-stepfunctions (>=1.28.0,<1.29.0)"] 545 | storagegateway = ["mypy-boto3-storagegateway (>=1.28.0,<1.29.0)"] 546 | sts = ["mypy-boto3-sts (>=1.28.0,<1.29.0)"] 547 | support = ["mypy-boto3-support (>=1.28.0,<1.29.0)"] 548 | support-app = ["mypy-boto3-support-app (>=1.28.0,<1.29.0)"] 549 | swf = ["mypy-boto3-swf (>=1.28.0,<1.29.0)"] 550 | synthetics = ["mypy-boto3-synthetics (>=1.28.0,<1.29.0)"] 551 | textract = ["mypy-boto3-textract (>=1.28.0,<1.29.0)"] 552 | timestream-query = ["mypy-boto3-timestream-query (>=1.28.0,<1.29.0)"] 553 | timestream-write = ["mypy-boto3-timestream-write (>=1.28.0,<1.29.0)"] 554 | tnb = ["mypy-boto3-tnb (>=1.28.0,<1.29.0)"] 555 | transcribe = ["mypy-boto3-transcribe (>=1.28.0,<1.29.0)"] 556 | transfer = ["mypy-boto3-transfer (>=1.28.0,<1.29.0)"] 557 | translate = ["mypy-boto3-translate (>=1.28.0,<1.29.0)"] 558 | verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.28.0,<1.29.0)"] 559 | voice-id = ["mypy-boto3-voice-id (>=1.28.0,<1.29.0)"] 560 | vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.28.0,<1.29.0)"] 561 | waf = ["mypy-boto3-waf (>=1.28.0,<1.29.0)"] 562 | waf-regional = ["mypy-boto3-waf-regional (>=1.28.0,<1.29.0)"] 563 | wafv2 = ["mypy-boto3-wafv2 (>=1.28.0,<1.29.0)"] 564 | wellarchitected = ["mypy-boto3-wellarchitected (>=1.28.0,<1.29.0)"] 565 | wisdom = ["mypy-boto3-wisdom (>=1.28.0,<1.29.0)"] 566 | workdocs = ["mypy-boto3-workdocs (>=1.28.0,<1.29.0)"] 567 | worklink = ["mypy-boto3-worklink (>=1.28.0,<1.29.0)"] 568 | workmail = ["mypy-boto3-workmail (>=1.28.0,<1.29.0)"] 569 | workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.28.0,<1.29.0)"] 570 | workspaces = ["mypy-boto3-workspaces (>=1.28.0,<1.29.0)"] 571 | workspaces-web = ["mypy-boto3-workspaces-web (>=1.28.0,<1.29.0)"] 572 | xray = ["mypy-boto3-xray (>=1.28.0,<1.29.0)"] 573 | 574 | [[package]] 575 | name = "botocore" 576 | version = "1.31.15" 577 | description = "Low-level, data-driven core of boto 3." 578 | optional = false 579 | python-versions = ">= 3.7" 580 | files = [ 581 | {file = "botocore-1.31.15-py3-none-any.whl", hash = "sha256:b3a0f787f275711875476cbe12a0123b2e6570b2f505e2fa509dcec3c5410b57"}, 582 | {file = "botocore-1.31.15.tar.gz", hash = "sha256:b46d1ce4e0cf42d28fdf61ce0c999904645d38b51cb809817a361c0cec16d487"}, 583 | ] 584 | 585 | [package.dependencies] 586 | jmespath = ">=0.7.1,<2.0.0" 587 | python-dateutil = ">=2.1,<3.0.0" 588 | urllib3 = ">=1.25.4,<1.27" 589 | 590 | [package.extras] 591 | crt = ["awscrt (==0.16.26)"] 592 | 593 | [[package]] 594 | name = "botocore-stubs" 595 | version = "1.31.15" 596 | description = "Type annotations and code completion for botocore" 597 | optional = false 598 | python-versions = ">=3.7,<4.0" 599 | files = [ 600 | {file = "botocore_stubs-1.31.15-py3-none-any.whl", hash = "sha256:5dab719199319de5e1fbc35d904abffdf7fde05353edc49577d56a9bc61ac507"}, 601 | {file = "botocore_stubs-1.31.15.tar.gz", hash = "sha256:2415b5c41d51562be1f98ceedc0dc685a3268d77eddc6983017e943c39cfe939"}, 602 | ] 603 | 604 | [package.dependencies] 605 | types-awscrt = "*" 606 | 607 | [[package]] 608 | name = "cattrs" 609 | version = "23.1.2" 610 | description = "Composable complex class support for attrs and dataclasses." 611 | optional = false 612 | python-versions = ">=3.7" 613 | files = [ 614 | {file = "cattrs-23.1.2-py3-none-any.whl", hash = "sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4"}, 615 | {file = "cattrs-23.1.2.tar.gz", hash = "sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657"}, 616 | ] 617 | 618 | [package.dependencies] 619 | attrs = ">=20" 620 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 621 | typing_extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} 622 | 623 | [package.extras] 624 | bson = ["pymongo (>=4.2.0,<5.0.0)"] 625 | cbor2 = ["cbor2 (>=5.4.6,<6.0.0)"] 626 | msgpack = ["msgpack (>=1.0.2,<2.0.0)"] 627 | orjson = ["orjson (>=3.5.2,<4.0.0)"] 628 | pyyaml = ["PyYAML (>=6.0,<7.0)"] 629 | tomlkit = ["tomlkit (>=0.11.4,<0.12.0)"] 630 | ujson = ["ujson (>=5.4.0,<6.0.0)"] 631 | 632 | [[package]] 633 | name = "certifi" 634 | version = "2023.7.22" 635 | description = "Python package for providing Mozilla's CA Bundle." 636 | optional = false 637 | python-versions = ">=3.6" 638 | files = [ 639 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 640 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 641 | ] 642 | 643 | [[package]] 644 | name = "charset-normalizer" 645 | version = "3.2.0" 646 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 647 | optional = false 648 | python-versions = ">=3.7.0" 649 | files = [ 650 | {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, 651 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, 652 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, 653 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, 654 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, 655 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, 656 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, 657 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, 658 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, 659 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, 660 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, 661 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, 662 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, 663 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, 664 | {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, 665 | {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, 666 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, 667 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, 668 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, 669 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, 670 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, 671 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, 672 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, 673 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, 674 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, 675 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, 676 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, 677 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, 678 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, 679 | {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, 680 | {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, 681 | {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, 682 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, 683 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, 684 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, 685 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, 686 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, 687 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, 688 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, 689 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, 690 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, 691 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, 692 | {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, 693 | {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, 694 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, 695 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, 696 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, 697 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, 698 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, 699 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, 700 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, 701 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, 702 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, 703 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, 704 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, 705 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, 706 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, 707 | {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, 708 | {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, 709 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, 710 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, 711 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, 712 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, 713 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, 714 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, 715 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, 716 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, 717 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, 718 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, 719 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, 720 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, 721 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, 722 | {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, 723 | {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, 724 | {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, 725 | ] 726 | 727 | [[package]] 728 | name = "click" 729 | version = "8.1.6" 730 | description = "Composable command line interface toolkit" 731 | optional = false 732 | python-versions = ">=3.7" 733 | files = [ 734 | {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, 735 | {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, 736 | ] 737 | 738 | [package.dependencies] 739 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 740 | 741 | [[package]] 742 | name = "colorama" 743 | version = "0.4.6" 744 | description = "Cross-platform colored terminal text." 745 | optional = false 746 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 747 | files = [ 748 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 749 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 750 | ] 751 | 752 | [[package]] 753 | name = "constructs" 754 | version = "10.2.69" 755 | description = "A programming model for software-defined state" 756 | optional = false 757 | python-versions = "~=3.7" 758 | files = [ 759 | {file = "constructs-10.2.69-py3-none-any.whl", hash = "sha256:27a60f5ce4faa4d43c91c73f24e1a245c0a1ef67ea1c8a3df9ca6af9adf618df"}, 760 | {file = "constructs-10.2.69.tar.gz", hash = "sha256:520ddd665cc336df90be06bb1bd49f3a9a7400d886cad8aef7b0155593b4ffa4"}, 761 | ] 762 | 763 | [package.dependencies] 764 | jsii = ">=1.84.0,<2.0.0" 765 | publication = ">=0.0.3" 766 | typeguard = ">=2.13.3,<2.14.0" 767 | 768 | [[package]] 769 | name = "coverage" 770 | version = "7.2.7" 771 | description = "Code coverage measurement for Python" 772 | optional = false 773 | python-versions = ">=3.7" 774 | files = [ 775 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 776 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 777 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 778 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 779 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 780 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 781 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 782 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 783 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 784 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 785 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 786 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 787 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 788 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 789 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 790 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 791 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 792 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 793 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 794 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 795 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 796 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 797 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 798 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 799 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 800 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 801 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 802 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 803 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 804 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 805 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 806 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 807 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 808 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 809 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 810 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 811 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 812 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 813 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 814 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 815 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 816 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 817 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 818 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 819 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 820 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 821 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 822 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 823 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 824 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 825 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 826 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 827 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 828 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 829 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 830 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 831 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 832 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 833 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 834 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 835 | ] 836 | 837 | [package.dependencies] 838 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 839 | 840 | [package.extras] 841 | toml = ["tomli"] 842 | 843 | [[package]] 844 | name = "decorator" 845 | version = "5.1.1" 846 | description = "Decorators for Humans" 847 | optional = false 848 | python-versions = ">=3.5" 849 | files = [ 850 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 851 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 852 | ] 853 | 854 | [[package]] 855 | name = "dill" 856 | version = "0.3.7" 857 | description = "serialize all of Python" 858 | optional = false 859 | python-versions = ">=3.7" 860 | files = [ 861 | {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, 862 | {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, 863 | ] 864 | 865 | [package.extras] 866 | graph = ["objgraph (>=1.7.2)"] 867 | 868 | [[package]] 869 | name = "exceptiongroup" 870 | version = "1.1.2" 871 | description = "Backport of PEP 654 (exception groups)" 872 | optional = false 873 | python-versions = ">=3.7" 874 | files = [ 875 | {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, 876 | {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, 877 | ] 878 | 879 | [package.extras] 880 | test = ["pytest (>=6)"] 881 | 882 | [[package]] 883 | name = "flake8" 884 | version = "5.0.4" 885 | description = "the modular source code checker: pep8 pyflakes and co" 886 | optional = false 887 | python-versions = ">=3.6.1" 888 | files = [ 889 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 890 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 891 | ] 892 | 893 | [package.dependencies] 894 | mccabe = ">=0.7.0,<0.8.0" 895 | pycodestyle = ">=2.9.0,<2.10.0" 896 | pyflakes = ">=2.5.0,<2.6.0" 897 | 898 | [[package]] 899 | name = "idna" 900 | version = "3.4" 901 | description = "Internationalized Domain Names in Applications (IDNA)" 902 | optional = false 903 | python-versions = ">=3.5" 904 | files = [ 905 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 906 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 907 | ] 908 | 909 | [[package]] 910 | name = "importlib-resources" 911 | version = "6.0.0" 912 | description = "Read resources from Python packages" 913 | optional = false 914 | python-versions = ">=3.8" 915 | files = [ 916 | {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, 917 | {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, 918 | ] 919 | 920 | [package.dependencies] 921 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 922 | 923 | [package.extras] 924 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 925 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 926 | 927 | [[package]] 928 | name = "iniconfig" 929 | version = "2.0.0" 930 | description = "brain-dead simple config-ini parsing" 931 | optional = false 932 | python-versions = ">=3.7" 933 | files = [ 934 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 935 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 936 | ] 937 | 938 | [[package]] 939 | name = "isort" 940 | version = "5.12.0" 941 | description = "A Python utility / library to sort Python imports." 942 | optional = false 943 | python-versions = ">=3.8.0" 944 | files = [ 945 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 946 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 947 | ] 948 | 949 | [package.extras] 950 | colors = ["colorama (>=0.4.3)"] 951 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 952 | plugins = ["setuptools"] 953 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 954 | 955 | [[package]] 956 | name = "jmespath" 957 | version = "1.0.1" 958 | description = "JSON Matching Expressions" 959 | optional = false 960 | python-versions = ">=3.7" 961 | files = [ 962 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 963 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 964 | ] 965 | 966 | [[package]] 967 | name = "jsii" 968 | version = "1.85.0" 969 | description = "Python client for jsii runtime" 970 | optional = false 971 | python-versions = "~=3.7" 972 | files = [ 973 | {file = "jsii-1.85.0-py3-none-any.whl", hash = "sha256:379feb1a1a3c4e449307564f42a7cddef05e43760cbfbbfe8434f6448cd668a0"}, 974 | {file = "jsii-1.85.0.tar.gz", hash = "sha256:b77194cf053c06c6bdffc887a4d1d2a41113c6f4780a7d78d70a780a70998008"}, 975 | ] 976 | 977 | [package.dependencies] 978 | attrs = ">=21.2,<24.0" 979 | cattrs = ">=1.8,<23.2" 980 | importlib-resources = ">=5.2.0" 981 | publication = ">=0.0.3" 982 | python-dateutil = "*" 983 | typeguard = ">=2.13.3,<2.14.0" 984 | typing-extensions = ">=3.7,<5.0" 985 | 986 | [[package]] 987 | name = "lazy-object-proxy" 988 | version = "1.9.0" 989 | description = "A fast and thorough lazy object proxy." 990 | optional = false 991 | python-versions = ">=3.7" 992 | files = [ 993 | {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, 994 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, 995 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, 996 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, 997 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, 998 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, 999 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, 1000 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, 1001 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, 1002 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, 1003 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, 1004 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, 1005 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, 1006 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, 1007 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, 1008 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, 1009 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, 1010 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, 1011 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, 1012 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, 1013 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, 1014 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, 1015 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, 1016 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, 1017 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, 1018 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, 1019 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, 1020 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, 1021 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, 1022 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, 1023 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, 1024 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, 1025 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, 1026 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, 1027 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, 1028 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "mccabe" 1033 | version = "0.7.0" 1034 | description = "McCabe checker, plugin for flake8" 1035 | optional = false 1036 | python-versions = ">=3.6" 1037 | files = [ 1038 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 1039 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "mypy" 1044 | version = "0.982" 1045 | description = "Optional static typing for Python" 1046 | optional = false 1047 | python-versions = ">=3.7" 1048 | files = [ 1049 | {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, 1050 | {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, 1051 | {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, 1052 | {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, 1053 | {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, 1054 | {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, 1055 | {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, 1056 | {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, 1057 | {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, 1058 | {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, 1059 | {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, 1060 | {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, 1061 | {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, 1062 | {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, 1063 | {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, 1064 | {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, 1065 | {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, 1066 | {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, 1067 | {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, 1068 | {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, 1069 | {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, 1070 | {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, 1071 | {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, 1072 | {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, 1073 | ] 1074 | 1075 | [package.dependencies] 1076 | mypy-extensions = ">=0.4.3" 1077 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 1078 | typing-extensions = ">=3.10" 1079 | 1080 | [package.extras] 1081 | dmypy = ["psutil (>=4.0)"] 1082 | python2 = ["typed-ast (>=1.4.0,<2)"] 1083 | reports = ["lxml"] 1084 | 1085 | [[package]] 1086 | name = "mypy-boto3" 1087 | version = "1.28.15.post1" 1088 | description = "Type annotations for boto3 1.28.15 master module generated with mypy-boto3-builder 7.16.2" 1089 | optional = false 1090 | python-versions = ">=3.7" 1091 | files = [ 1092 | {file = "mypy-boto3-1.28.15.post1.tar.gz", hash = "sha256:9d5b01b54e18b5537f68578e13f4cb5eb58d242cce46b8bd369835dae23e78b5"}, 1093 | {file = "mypy_boto3-1.28.15.post1-py3-none-any.whl", hash = "sha256:b31799f274d98431e968d93106b4c8682f04661ed845761a6e0f6c89bdbebb5f"}, 1094 | ] 1095 | 1096 | [package.dependencies] 1097 | boto3 = "*" 1098 | 1099 | [[package]] 1100 | name = "mypy-boto3-cloudformation" 1101 | version = "1.28.15.post1" 1102 | description = "Type annotations for boto3.CloudFormation 1.28.15 service generated with mypy-boto3-builder 7.16.2" 1103 | optional = false 1104 | python-versions = ">=3.7" 1105 | files = [ 1106 | {file = "mypy-boto3-cloudformation-1.28.15.post1.tar.gz", hash = "sha256:8e31bc4ade0fea3b31edad21f72517f4f17e970aa2e78beb51d3e039b9a5ad1c"}, 1107 | {file = "mypy_boto3_cloudformation-1.28.15.post1-py3-none-any.whl", hash = "sha256:1916acd4010e9b3aa20524b7c7ac7d568d7dcdfff414b251ea7a96bfb05109b1"}, 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "mypy-boto3-dynamodb" 1112 | version = "1.28.15.post2" 1113 | description = "Type annotations for boto3.DynamoDB 1.28.15 service generated with mypy-boto3-builder 7.16.2" 1114 | optional = false 1115 | python-versions = ">=3.7" 1116 | files = [ 1117 | {file = "mypy-boto3-dynamodb-1.28.15.post2.tar.gz", hash = "sha256:54508d8b39ccd11f474d9b94ce753ce5395fc972293d6fb68797d549574670e4"}, 1118 | {file = "mypy_boto3_dynamodb-1.28.15.post2-py3-none-any.whl", hash = "sha256:798822426c826187a182cb6a561a43be4f94cd8d654ce34a40a2b6ee566a017e"}, 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "mypy-boto3-lambda" 1123 | version = "1.28.15.post1" 1124 | description = "Type annotations for boto3.Lambda 1.28.15 service generated with mypy-boto3-builder 7.16.2" 1125 | optional = false 1126 | python-versions = ">=3.7" 1127 | files = [ 1128 | {file = "mypy-boto3-lambda-1.28.15.post1.tar.gz", hash = "sha256:b58785e66a2026b3c12764e2ed2ac244f4a37f64e3e5c74b46aba9f5c9664178"}, 1129 | {file = "mypy_boto3_lambda-1.28.15.post1-py3-none-any.whl", hash = "sha256:dde62ffaa4c410275ea5fd617c62cf4513fe71ec5af0d03fd42ed518be557108"}, 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "mypy-boto3-s3" 1134 | version = "1.28.15.post1" 1135 | description = "Type annotations for boto3.S3 1.28.15 service generated with mypy-boto3-builder 7.16.2" 1136 | optional = false 1137 | python-versions = ">=3.7" 1138 | files = [ 1139 | {file = "mypy-boto3-s3-1.28.15.post1.tar.gz", hash = "sha256:65502be825789fd16e4cacf10aecd6408554b60ae5d261be60fd46bee69d8592"}, 1140 | {file = "mypy_boto3_s3-1.28.15.post1-py3-none-any.whl", hash = "sha256:2e7d26fd2b56a13843afdba88576185aaa16aa7493f02576e3e94337be29f56c"}, 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "mypy-boto3-sqs" 1145 | version = "1.28.15.post1" 1146 | description = "Type annotations for boto3.SQS 1.28.15 service generated with mypy-boto3-builder 7.16.2" 1147 | optional = false 1148 | python-versions = ">=3.7" 1149 | files = [ 1150 | {file = "mypy-boto3-sqs-1.28.15.post1.tar.gz", hash = "sha256:d4c8caf7bbdc322025fe88c1bf377138fe228642fb1531526ede0b63b16815b3"}, 1151 | {file = "mypy_boto3_sqs-1.28.15.post1-py3-none-any.whl", hash = "sha256:ea1958442dbbfee65533b49b46d61d74d95316e2f12d1e285627bf08e92ed6a1"}, 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "mypy-extensions" 1156 | version = "1.0.0" 1157 | description = "Type system extensions for programs checked with the mypy type checker." 1158 | optional = false 1159 | python-versions = ">=3.5" 1160 | files = [ 1161 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 1162 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "packaging" 1167 | version = "23.1" 1168 | description = "Core utilities for Python packages" 1169 | optional = false 1170 | python-versions = ">=3.7" 1171 | files = [ 1172 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 1173 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "pathspec" 1178 | version = "0.11.2" 1179 | description = "Utility library for gitignore style pattern matching of file paths." 1180 | optional = false 1181 | python-versions = ">=3.7" 1182 | files = [ 1183 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 1184 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "platformdirs" 1189 | version = "3.10.0" 1190 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 1191 | optional = false 1192 | python-versions = ">=3.7" 1193 | files = [ 1194 | {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, 1195 | {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, 1196 | ] 1197 | 1198 | [package.extras] 1199 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 1200 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 1201 | 1202 | [[package]] 1203 | name = "pluggy" 1204 | version = "1.2.0" 1205 | description = "plugin and hook calling mechanisms for python" 1206 | optional = false 1207 | python-versions = ">=3.7" 1208 | files = [ 1209 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 1210 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 1211 | ] 1212 | 1213 | [package.extras] 1214 | dev = ["pre-commit", "tox"] 1215 | testing = ["pytest", "pytest-benchmark"] 1216 | 1217 | [[package]] 1218 | name = "publication" 1219 | version = "0.0.3" 1220 | description = "Publication helps you maintain public-api-friendly modules by preventing unintentional access to private implementation details via introspection." 1221 | optional = false 1222 | python-versions = "*" 1223 | files = [ 1224 | {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, 1225 | {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "py" 1230 | version = "1.11.0" 1231 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 1232 | optional = false 1233 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 1234 | files = [ 1235 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 1236 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "pycodestyle" 1241 | version = "2.9.1" 1242 | description = "Python style guide checker" 1243 | optional = false 1244 | python-versions = ">=3.6" 1245 | files = [ 1246 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 1247 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "pyflakes" 1252 | version = "2.5.0" 1253 | description = "passive checker of Python programs" 1254 | optional = false 1255 | python-versions = ">=3.6" 1256 | files = [ 1257 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 1258 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "pylint" 1263 | version = "2.17.5" 1264 | description = "python code static checker" 1265 | optional = false 1266 | python-versions = ">=3.7.2" 1267 | files = [ 1268 | {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, 1269 | {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, 1270 | ] 1271 | 1272 | [package.dependencies] 1273 | astroid = ">=2.15.6,<=2.17.0-dev0" 1274 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 1275 | dill = [ 1276 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 1277 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 1278 | ] 1279 | isort = ">=4.2.5,<6" 1280 | mccabe = ">=0.6,<0.8" 1281 | platformdirs = ">=2.2.0" 1282 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 1283 | tomlkit = ">=0.10.1" 1284 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 1285 | 1286 | [package.extras] 1287 | spelling = ["pyenchant (>=3.2,<4.0)"] 1288 | testutils = ["gitpython (>3)"] 1289 | 1290 | [[package]] 1291 | name = "pytest" 1292 | version = "7.4.0" 1293 | description = "pytest: simple powerful testing with Python" 1294 | optional = false 1295 | python-versions = ">=3.7" 1296 | files = [ 1297 | {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, 1298 | {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, 1299 | ] 1300 | 1301 | [package.dependencies] 1302 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1303 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 1304 | iniconfig = "*" 1305 | packaging = "*" 1306 | pluggy = ">=0.12,<2.0" 1307 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 1308 | 1309 | [package.extras] 1310 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1311 | 1312 | [[package]] 1313 | name = "pytest-cov" 1314 | version = "4.1.0" 1315 | description = "Pytest plugin for measuring coverage." 1316 | optional = false 1317 | python-versions = ">=3.7" 1318 | files = [ 1319 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 1320 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 1321 | ] 1322 | 1323 | [package.dependencies] 1324 | coverage = {version = ">=5.2.1", extras = ["toml"]} 1325 | pytest = ">=4.6" 1326 | 1327 | [package.extras] 1328 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1329 | 1330 | [[package]] 1331 | name = "python-dateutil" 1332 | version = "2.8.2" 1333 | description = "Extensions to the standard Python datetime module" 1334 | optional = false 1335 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1336 | files = [ 1337 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1338 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1339 | ] 1340 | 1341 | [package.dependencies] 1342 | six = ">=1.5" 1343 | 1344 | [[package]] 1345 | name = "requests" 1346 | version = "2.31.0" 1347 | description = "Python HTTP for Humans." 1348 | optional = false 1349 | python-versions = ">=3.7" 1350 | files = [ 1351 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 1352 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 1353 | ] 1354 | 1355 | [package.dependencies] 1356 | certifi = ">=2017.4.17" 1357 | charset-normalizer = ">=2,<4" 1358 | idna = ">=2.5,<4" 1359 | urllib3 = ">=1.21.1,<3" 1360 | 1361 | [package.extras] 1362 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1363 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1364 | 1365 | [[package]] 1366 | name = "retry" 1367 | version = "0.9.2" 1368 | description = "Easy to use retry decorator." 1369 | optional = false 1370 | python-versions = "*" 1371 | files = [ 1372 | {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, 1373 | {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, 1374 | ] 1375 | 1376 | [package.dependencies] 1377 | decorator = ">=3.4.2" 1378 | py = ">=1.4.26,<2.0.0" 1379 | 1380 | [[package]] 1381 | name = "s3transfer" 1382 | version = "0.6.1" 1383 | description = "An Amazon S3 Transfer Manager" 1384 | optional = false 1385 | python-versions = ">= 3.7" 1386 | files = [ 1387 | {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, 1388 | {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, 1389 | ] 1390 | 1391 | [package.dependencies] 1392 | botocore = ">=1.12.36,<2.0a.0" 1393 | 1394 | [package.extras] 1395 | crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] 1396 | 1397 | [[package]] 1398 | name = "six" 1399 | version = "1.16.0" 1400 | description = "Python 2 and 3 compatibility utilities" 1401 | optional = false 1402 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1403 | files = [ 1404 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1405 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "tomli" 1410 | version = "2.0.1" 1411 | description = "A lil' TOML parser" 1412 | optional = false 1413 | python-versions = ">=3.7" 1414 | files = [ 1415 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1416 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "tomlkit" 1421 | version = "0.12.1" 1422 | description = "Style preserving TOML library" 1423 | optional = false 1424 | python-versions = ">=3.7" 1425 | files = [ 1426 | {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, 1427 | {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, 1428 | ] 1429 | 1430 | [[package]] 1431 | name = "typeguard" 1432 | version = "2.13.3" 1433 | description = "Run-time type checker for Python" 1434 | optional = false 1435 | python-versions = ">=3.5.3" 1436 | files = [ 1437 | {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, 1438 | {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, 1439 | ] 1440 | 1441 | [package.extras] 1442 | doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 1443 | test = ["mypy", "pytest", "typing-extensions"] 1444 | 1445 | [[package]] 1446 | name = "types-awscrt" 1447 | version = "0.17.0" 1448 | description = "Type annotations and code completion for awscrt" 1449 | optional = false 1450 | python-versions = ">=3.7,<4.0" 1451 | files = [ 1452 | {file = "types_awscrt-0.17.0-py3-none-any.whl", hash = "sha256:278c1a913098e226769cd28dc37643a2f360907353a7e8918e1bcdf93d4e34f2"}, 1453 | {file = "types_awscrt-0.17.0.tar.gz", hash = "sha256:4214783a747af900a5f98ec020d52ecae5910b470fd636813637a45b82a97516"}, 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "types-s3transfer" 1458 | version = "0.6.1" 1459 | description = "Type annotations and code completion for s3transfer" 1460 | optional = false 1461 | python-versions = ">=3.7,<4.0" 1462 | files = [ 1463 | {file = "types_s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:6d1ac1dedac750d570428362acdf60fdd4f277b0788855c3894d3226756b2bfb"}, 1464 | {file = "types_s3transfer-0.6.1.tar.gz", hash = "sha256:75ac1d7143d58c1e6af467cfd4a96c67ee058a3adf7c249d9309999e1f5f41e4"}, 1465 | ] 1466 | 1467 | [package.dependencies] 1468 | types-awscrt = "*" 1469 | 1470 | [[package]] 1471 | name = "typing-extensions" 1472 | version = "4.7.1" 1473 | description = "Backported and Experimental Type Hints for Python 3.7+" 1474 | optional = false 1475 | python-versions = ">=3.7" 1476 | files = [ 1477 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 1478 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "urllib3" 1483 | version = "1.26.18" 1484 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1485 | optional = false 1486 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 1487 | files = [ 1488 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, 1489 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, 1490 | ] 1491 | 1492 | [package.extras] 1493 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 1494 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 1495 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 1496 | 1497 | [[package]] 1498 | name = "wrapt" 1499 | version = "1.15.0" 1500 | description = "Module for decorators, wrappers and monkey patching." 1501 | optional = false 1502 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 1503 | files = [ 1504 | {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, 1505 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, 1506 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, 1507 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, 1508 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, 1509 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, 1510 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, 1511 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, 1512 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, 1513 | {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, 1514 | {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, 1515 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, 1516 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, 1517 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, 1518 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, 1519 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, 1520 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, 1521 | {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, 1522 | {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, 1523 | {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, 1524 | {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, 1525 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, 1526 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, 1527 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, 1528 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, 1529 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, 1530 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, 1531 | {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, 1532 | {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, 1533 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, 1534 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, 1535 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, 1536 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, 1537 | {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, 1538 | {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, 1539 | {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, 1540 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, 1541 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, 1542 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, 1543 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, 1544 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, 1545 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, 1546 | {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, 1547 | {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, 1548 | {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, 1549 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, 1550 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, 1551 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, 1552 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, 1553 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, 1554 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, 1555 | {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, 1556 | {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, 1557 | {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, 1558 | {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, 1559 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, 1560 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, 1561 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, 1562 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, 1563 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, 1564 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, 1565 | {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, 1566 | {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, 1567 | {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, 1568 | {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, 1569 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, 1570 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, 1571 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, 1572 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, 1573 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, 1574 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, 1575 | {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, 1576 | {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, 1577 | {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, 1578 | {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "zipp" 1583 | version = "3.16.2" 1584 | description = "Backport of pathlib-compatible object wrapper for zip files" 1585 | optional = false 1586 | python-versions = ">=3.8" 1587 | files = [ 1588 | {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, 1589 | {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, 1590 | ] 1591 | 1592 | [package.extras] 1593 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 1594 | testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 1595 | 1596 | [metadata] 1597 | lock-version = "2.0" 1598 | python-versions = "^3.9" 1599 | content-hash = "aad080e8c390221e19f8b21bafba317e34da39fe07748717525e1ee96dbafa54" 1600 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aws-lambda-python-cdk" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["walmsles <2704782+walmsles@users.noreply.github.com>"] 6 | readme = "README.md" 7 | packages = [ 8 | {include = "services"}, 9 | {include = "cdk"}, 10 | {include = "tests"} 11 | ] 12 | [tool.poetry.dependencies] 13 | python = "^3.9" 14 | 15 | 16 | [tool.poetry.group.dev.dependencies] 17 | aws-cdk-lib = "^2.49.0" 18 | constructs = "^10.1.144" 19 | boto3 = "^1.25.4" 20 | isort = "^5.10.1" 21 | black = "^22.10.0" 22 | flake8 = "^5.0.4" 23 | pytest = "^7.2.0" 24 | pytest-cov = "^4.0.0" 25 | aws-cdk-aws-lambda-python-alpha = "^2.49.0a0" 26 | mypy = "^0.982" 27 | mypy-boto3-s3 = "^1.26.0.post1" 28 | mypy-boto3 = "^1.26.0.post1" 29 | boto3-stubs = "^1.26.0.post1" 30 | pylint = "^2.15.5" 31 | mypy-boto3-cloudformation = "^1.26.0.post1" 32 | mypy-boto3-lambda = "^1.26.0.post1" 33 | requests = "^2.28.1" 34 | retry = "^0.9.2" 35 | mypy-boto3-sqs = "^1.26.0.post1" 36 | mypy-boto3-dynamodb = "^1.26.0.post1" 37 | 38 | 39 | [tool.poetry.group.event_api.dependencies] 40 | aws-lambda-powertools = "^2.0.0" 41 | 42 | [tool.poetry.group.router.dependencies] 43 | 44 | 45 | [tool.poetry.group.event_processor.dependencies] 46 | aws-lambda-powertools = "^2.2.0" 47 | mypy-boto3-dynamodb = "^1.26.0.post1" 48 | 49 | [tool.isort] 50 | profile = "black" 51 | 52 | [build-system] 53 | requires = ["poetry-core"] 54 | build-backend = "poetry.core.masonry.api" 55 | -------------------------------------------------------------------------------- /scripts/docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | podman "$@" 4 | 5 | if [ $? == 125 ]; then 6 | exit 1; 7 | fi 8 | -------------------------------------------------------------------------------- /scripts/make-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for folder in $(ls -d ${WORKDIR:-${PWD}}/services/* | grep -v __pycache__) 4 | do 5 | if [[ -d ${folder} ]]; then 6 | group=$(basename ${folder}) 7 | 8 | # check if service group exists (to avoid poetry errors) 9 | poetry show --with=${group} --quiet 10 | group_exists=$? 11 | all_groups="" 12 | if [[ $group_exists -eq 0 ]]; then 13 | # export main dependencies AND specific group dependencies 14 | echo creating requirements in ${folder} 15 | 16 | # collect groups to export for testing which requires ALL deps 17 | all_groups="${all_groups} --with=${group}" 18 | poetry export --without-hashes --with=${group} --with main > ${folder}/runtime/requirements.txt 19 | else 20 | # export main only - so get centrally installed dependencies 21 | echo creating requirements in ${folder} 22 | poetry export --without-hashes --with main > ${folder}/runtime/requirements.txt 23 | fi 24 | fi 25 | done 26 | 27 | # export for testing 28 | poetry export --without-hashes --with dev $all_groups > ${WORKDIR:-${PWD}}/tests/requirements.txt 29 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/services/__init__.py -------------------------------------------------------------------------------- /services/event_api/infrastructure.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from pathlib import Path 3 | from typing import Optional 4 | 5 | from aws_cdk import Duration 6 | from aws_cdk.aws_apigateway import LambdaIntegration, RestApi 7 | from aws_cdk.aws_lambda import Architecture, Runtime, Tracing 8 | from aws_cdk.aws_lambda_python_alpha import PythonFunction 9 | from aws_cdk.aws_s3 import BlockPublicAccess, Bucket, BucketEncryption, LifecycleRule 10 | from aws_cdk.aws_sqs import DeadLetterQueue, Queue, QueueEncryption 11 | from constructs import Construct 12 | 13 | 14 | class EventFunctionConstruct(Construct): 15 | def __init__( 16 | self, 17 | scope: "Construct", 18 | id: builtins.str, 19 | *, 20 | bucket: Optional[Bucket] = None, 21 | queue: Optional[Queue] = None, 22 | ) -> None: 23 | """ 24 | EventApiConstruct 25 | ----------------- 26 | Component Construct for EventAPI inbound service responsible for validating the inbound event, storing in s3 27 | and submitting meta-data into SNS for downstream processing. 28 | 29 | Parameters 30 | ---------- 31 | bucket : allow the bucket to be associated with the EVentAPI service to be injected, 32 | if None is provided one will be created. 33 | queue : Allow the SQS Queue for use by EventApi Lambda to be injected, if None provided one will be created. 34 | """ 35 | super().__init__(scope, id) 36 | 37 | self.bucket = bucket 38 | self.queue = queue 39 | 40 | self._create_bucket() 41 | self._create_queue() 42 | 43 | self.function = PythonFunction( 44 | self, 45 | "LambdaFunction", 46 | runtime=Runtime.PYTHON_3_9, 47 | entry=str(Path(__file__).parent.joinpath("runtime").resolve()), 48 | index="api.py", 49 | handler="lambda_handler", 50 | architecture=Architecture.X86_64, 51 | tracing=Tracing.ACTIVE, 52 | environment={ 53 | "EVENT_BUCKET": self.bucket.bucket_name, 54 | "EVENT_QUEUE": self.queue.queue_url, 55 | }, 56 | ) 57 | 58 | # Grant bucket read/write 59 | self.bucket.grant_read_write(self.function) 60 | self.queue.grant_send_messages(self.function) 61 | 62 | def _create_bucket(self): 63 | # Bucket may be provided to this construct if required. If not one will be created 64 | if self.bucket is None: 65 | # Store events for 7 days then remove 66 | life_cycles = [ 67 | LifecycleRule( 68 | expiration=Duration.days(7), 69 | ) 70 | ] 71 | # Create Event Store Bucket 72 | self.bucket = Bucket( 73 | self, 74 | "EventStore", 75 | encryption=BucketEncryption.KMS_MANAGED, 76 | bucket_key_enabled=True, 77 | block_public_access=BlockPublicAccess.BLOCK_ALL, 78 | lifecycle_rules=life_cycles, 79 | ) 80 | 81 | def _create_queue(self): 82 | if self.queue is None: 83 | self.dlq = Queue( 84 | self, 85 | "MessageDLQ", 86 | encryption=QueueEncryption.KMS_MANAGED, 87 | enforce_ssl=True, 88 | retention_period=Duration.days(14), 89 | ) 90 | self.queue = Queue( 91 | self, 92 | "MessageQueue", 93 | encryption=QueueEncryption.KMS_MANAGED, 94 | enforce_ssl=True, 95 | dead_letter_queue=DeadLetterQueue(queue=self.dlq, max_receive_count=5), 96 | ) 97 | 98 | 99 | class EventApiConstruct(Construct): 100 | def __init__( 101 | self, 102 | scope: "Construct", 103 | id: builtins.str, 104 | *, 105 | bucket: Optional[Bucket] = None, 106 | queue: Optional[Queue] = None, 107 | ) -> None: 108 | """ 109 | EventApiConstruct 110 | ----------------- 111 | Component Construct for EventAPI inbound service responsible for validating the inbound event, storing in s3 112 | and submitting meta-data into SNS for downstream processing. 113 | 114 | Parameters 115 | ---------- 116 | bucket : allow the bucket to be associated with the EVentAPI service to be injected, 117 | if None is provided one will be created. 118 | queue : Allow the SQS Queue for use by EventApi Lambda to be injected, if None provided one will be created. 119 | """ 120 | super().__init__(scope, id) 121 | 122 | self.event_api: EventFunctionConstruct = EventFunctionConstruct( 123 | self, "EventFunction", bucket=bucket, queue=queue 124 | ) 125 | 126 | # Bind to REST API V1 127 | self.api = RestApi(self, "EventApi") 128 | events = self.api.root.add_resource("events") 129 | events.add_method( 130 | "POST", LambdaIntegration(self.event_api.function, proxy=True) 131 | ) 132 | -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/services/event_api/runtime/adapters/__init__.py -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/file_storage.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import TYPE_CHECKING, Any, Dict 4 | 5 | import boto3 6 | 7 | from .ports.file_port import FileStoragePort 8 | 9 | if TYPE_CHECKING: 10 | from mypy_boto3_s3 import S3Client 11 | 12 | 13 | class FileStorage(FileStoragePort): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | self.s3_client: "S3Client" = boto3.client("s3") 18 | 19 | # read bucket name from envionrment variable 20 | self.bucket_name = os.environ.get("EVENT_BUCKET", "bucket") 21 | 22 | def read_file(self, filename: str) -> str: 23 | file_object = self.s3_client.get_object(Bucket=self.bucket_name, Key=filename) 24 | data: str = json.loads(file_object["Body"].read()) 25 | return data 26 | 27 | def save_file(self, filename: str, content: Dict[str, Any]) -> None: 28 | self.s3_client.put_object( 29 | Body=json.dumps(content), 30 | Bucket=self.bucket_name, 31 | Key=f"{filename}.json", 32 | ) 33 | -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/message.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING, Any, Dict, Optional 3 | 4 | import boto3 5 | 6 | from .ports.message_port import MessagePort 7 | 8 | if TYPE_CHECKING: 9 | from mypy_boto3_sqs import SQSClient 10 | 11 | 12 | class MessageProvider(MessagePort): 13 | def __init__(self, resource_id: str): 14 | super().__init__(resource_id) 15 | 16 | self._client: "SQSClient" = boto3.client("sqs") 17 | 18 | def send_event( 19 | self, message_id: str, body: Dict[str, Any], group_id: Optional[str] = None 20 | ): 21 | self.group_id = group_id 22 | message_body = {"message_id": message_id, "body": body} 23 | 24 | # Submit Message to SQS Queue 25 | self._client.send_message( 26 | QueueUrl=self._resource_id, MessageBody=json.dumps(message_body) 27 | ) 28 | -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/ports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/services/event_api/runtime/adapters/ports/__init__.py -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/ports/file_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict 3 | 4 | 5 | class FileStoragePort(ABC): 6 | def __init__(self): 7 | pass 8 | 9 | @abstractmethod 10 | def save_file(self, filename: str, content: Dict[str, Any]): 11 | pass 12 | 13 | @abstractmethod 14 | def read_file(self, filename: str) -> str: 15 | pass 16 | -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/ports/message_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Optional 3 | 4 | 5 | class MessagePort(ABC): 6 | """ 7 | Message Provider 8 | ---------------- 9 | Message provider abstract class to provide a port for sending messages to cloud infrastructure. 10 | Messages will have a message_id and a body and an optional group_id to enable grouping of messages. 11 | """ 12 | 13 | def __init__(self, resource_id: str): 14 | self._resource_id = resource_id 15 | 16 | @abstractmethod 17 | def send_event( 18 | self, message_id: str, body: Dict[str, Any], group_id: Optional[str] 19 | ): 20 | pass 21 | -------------------------------------------------------------------------------- /services/event_api/runtime/adapters/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict 3 | 4 | from .file_storage import FileStorage 5 | from .message import MessageProvider 6 | from .ports.file_port import FileStoragePort 7 | from .ports.message_port import MessagePort 8 | 9 | 10 | class EventService: 11 | def __init__( 12 | self, 13 | file_storage: FileStoragePort = None, 14 | message_provider: MessagePort = None, 15 | ): 16 | self._file_storage = file_storage 17 | self._message_provider = message_provider 18 | self._create_file_storage() 19 | self._create_message_provider() 20 | 21 | def _create_file_storage(self): 22 | if self._file_storage is None: 23 | self._file_storage = FileStorage() 24 | 25 | def _create_message_provider(self): 26 | if self._message_provider is None: 27 | sqs_queue_url = os.environ.get("EVENT_QUEUE", "queue_url") 28 | self._message_provider = MessageProvider(resource_id=sqs_queue_url) 29 | 30 | def process_event(self, transaction_id: str, event_detail: Dict[str, Any]): 31 | self._file_storage.save_file(transaction_id, event_detail) 32 | self._message_provider.send_event( 33 | message_id=transaction_id, body=event_detail, group_id=None 34 | ) 35 | return True 36 | -------------------------------------------------------------------------------- /services/event_api/runtime/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http import HTTPStatus 3 | from typing import Any, Dict 4 | 5 | import aws_lambda_powertools.event_handler.content_types as content_types 6 | from adapters.service import EventService 7 | from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response 8 | from aws_lambda_powertools.event_handler.exceptions import InternalServerError 9 | from aws_lambda_powertools.logging import Logger 10 | 11 | app = APIGatewayRestResolver() 12 | logger = Logger() 13 | 14 | event_service = EventService() 15 | 16 | 17 | @app.post("/events") 18 | def post_event(): 19 | transaction_id = app.current_event.request_context.request_id 20 | 21 | response = event_service.process_event( 22 | transaction_id, 23 | app.current_event.json_body, 24 | ) 25 | logger.info(response) 26 | 27 | if response: 28 | res_obj = {"transaction_id": transaction_id} 29 | return Response( 30 | status_code=HTTPStatus.OK.value, 31 | body=json.dumps(res_obj), 32 | content_type=content_types.APPLICATION_JSON, 33 | ) 34 | else: 35 | raise (InternalServerError("Processing failure")) 36 | 37 | 38 | @logger.inject_lambda_context(log_event=True) 39 | def lambda_handler(event, context) -> Dict[str, Any]: 40 | return app.resolve(event, context) 41 | -------------------------------------------------------------------------------- /services/event_processor/infrastructure.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from pathlib import Path 3 | 4 | from aws_cdk.aws_dynamodb import Attribute, AttributeType, Table, TableEncryption 5 | from aws_cdk.aws_lambda import Architecture, Runtime, Tracing 6 | from aws_cdk.aws_lambda_event_sources import SqsEventSource 7 | from aws_cdk.aws_lambda_python_alpha import PythonFunction 8 | from aws_cdk.aws_sqs import Queue 9 | from constructs import Construct 10 | 11 | 12 | class EventProcessorConstruct(Construct): 13 | def __init__( 14 | self, 15 | scope: "Construct", 16 | id: builtins.str, 17 | *, 18 | queue: Queue, 19 | ) -> None: 20 | """ 21 | EventApiConstruct 22 | ----------------- 23 | Component Construct for EventAPI inbound service responsible for validating the inbound event, storing in s3 24 | and submitting meta-data into SNS for downstream processing. 25 | 26 | Parameters 27 | ---------- 28 | queue : The message queue to be used as the trigger for the lambda function 29 | """ 30 | super().__init__(scope, id) 31 | 32 | self.table = Table( 33 | self, 34 | "EventStore", 35 | partition_key=Attribute(name="message_id", type=AttributeType.STRING), 36 | encryption=TableEncryption.AWS_MANAGED, 37 | ) 38 | 39 | self.function = PythonFunction( 40 | self, 41 | "LambdaFunction", 42 | runtime=Runtime.PYTHON_3_9, 43 | entry=str(Path(__file__).parent.joinpath("runtime").resolve()), 44 | index="processor.py", 45 | handler="lambda_handler", 46 | architecture=Architecture.X86_64, 47 | tracing=Tracing.ACTIVE, 48 | environment={ 49 | "EVENT_STORE": self.table.table_name, 50 | }, 51 | ) 52 | self.function.add_event_source( 53 | SqsEventSource( 54 | queue, 55 | batch_size=10, 56 | ) 57 | ) 58 | self.table.grant_write_data(self.function) 59 | -------------------------------------------------------------------------------- /services/event_processor/runtime/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/services/event_processor/runtime/adapters/__init__.py -------------------------------------------------------------------------------- /services/event_processor/runtime/adapters/ports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/services/event_processor/runtime/adapters/ports/__init__.py -------------------------------------------------------------------------------- /services/event_processor/runtime/adapters/ports/event_store.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict 3 | 4 | 5 | class EventStoragePort(ABC): 6 | @abstractmethod 7 | def store_event(self, message_id: str, content: Dict[str, Any]): 8 | pass 9 | -------------------------------------------------------------------------------- /services/event_processor/runtime/adapters/service.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from .ports.event_store import EventStoragePort 4 | from .table_store import TableStore 5 | 6 | 7 | class ProcessorService: 8 | def __init__(self, storage_service: Optional[EventStoragePort] = None) -> None: 9 | self._storage: EventStoragePort = storage_service 10 | self._create_storage() 11 | 12 | def _create_storage(self) -> None: 13 | if self._storage is None: 14 | self._storage = TableStore() 15 | 16 | def store_event(self, message_id: str, content: Dict[str, Any]) -> None: 17 | self._storage.store_event(message_id, content) 18 | -------------------------------------------------------------------------------- /services/event_processor/runtime/adapters/table_store.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict 3 | 4 | import boto3 5 | from mypy_boto3_dynamodb import DynamoDBClient 6 | 7 | from .ports.event_store import EventStoragePort 8 | 9 | 10 | class TableStore(EventStoragePort): 11 | def __init__(self) -> None: 12 | self._client: DynamoDBClient = boto3.resource("dynamodb") 13 | self._table = self._client.Table(os.environ.get("EVENT_STORE", "EventStore")) 14 | 15 | def store_event(self, message_id: str, content: Dict[str, Any]): 16 | self._table.put_item( 17 | Item={ 18 | "message_id": message_id, 19 | "body": content, 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /services/event_processor/runtime/processor.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from adapters.service import ProcessorService 4 | from aws_lambda_powertools.logging import Logger 5 | from aws_lambda_powertools.utilities.batch import ( 6 | BatchProcessor, 7 | EventType, 8 | batch_processor, 9 | ) 10 | from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord 11 | from aws_lambda_powertools.utilities.typing import LambdaContext 12 | 13 | logger = Logger() 14 | processor = BatchProcessor(event_type=EventType.SQS) 15 | service: ProcessorService = ProcessorService() 16 | 17 | 18 | def record_handler(record: SQSRecord): 19 | payload: str = record.body 20 | if payload: 21 | event: dict = json.loads(payload) 22 | message_id = event.get("message_id") 23 | content = event.get("body") 24 | service.store_event(message_id, content) 25 | 26 | 27 | @logger.inject_lambda_context(log_event=True) 28 | @batch_processor(record_handler=record_handler, processor=processor) 29 | def lambda_handler(event, context: LambdaContext): 30 | return processor.response() 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/__init__.py -------------------------------------------------------------------------------- /tests/e2e/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/e2e/api/__init__.py -------------------------------------------------------------------------------- /tests/e2e/api/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.e2e.api.infrastructure import EndToEndApiStack 4 | 5 | 6 | @pytest.fixture(autouse=True, scope="module") 7 | def infrastructure(): 8 | """Setup and teardown logic for E2E test infrastructure 9 | 10 | Yields 11 | ------ 12 | Dict[str, str] 13 | CloudFormation Outputs from deployed infrastructure 14 | """ 15 | stack = EndToEndApiStack() 16 | try: 17 | yield stack.deploy() 18 | finally: 19 | stack.delete() 20 | -------------------------------------------------------------------------------- /tests/e2e/api/infrastructure.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import RemovalPolicy, aws_logs 2 | 3 | from services.event_api.infrastructure import EventApiConstruct 4 | from services.event_processor.infrastructure import EventProcessorConstruct 5 | from tests.utils.base_infrastructure import BaseInfrastructure 6 | 7 | EVENT_API_URL = "EventApiUrl" 8 | 9 | 10 | class EndToEndApiStack(BaseInfrastructure): 11 | def create_resources(self): 12 | event_construct = EventApiConstruct(self.stack, "Api") 13 | EventProcessorConstruct( 14 | self.stack, "Processor", queue=event_construct.event_api.queue 15 | ) 16 | 17 | # Set expiry on Lambda log groups so they self delete when no longer needed 18 | # 19 | aws_logs.LogGroup( 20 | self.stack, 21 | "EventAPIFunction-lg", 22 | log_group_name=f"/aws/lamdba/{event_construct.event_api.function.function_name}", 23 | retention=aws_logs.RetentionDays.ONE_DAY, 24 | removal_policy=RemovalPolicy.DESTROY, 25 | ) 26 | 27 | self.add_cfn_output(name=EVENT_API_URL, value=event_construct.api.url) 28 | -------------------------------------------------------------------------------- /tests/e2e/api/test_api.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import pytest 4 | from requests import Request 5 | 6 | from tests.e2e.api.infrastructure import EVENT_API_URL 7 | from tests.utils.fetcher import fetch_http_response 8 | 9 | 10 | @pytest.fixture() 11 | def event_api_url(infrastructure: Dict) -> str: 12 | print(infrastructure) 13 | return infrastructure.get(EVENT_API_URL) 14 | 15 | 16 | def test_event_api(event_api_url): 17 | # Given 18 | api_data = {"body": "Hello World"} 19 | status_code = 200 20 | 21 | # When 22 | response = fetch_http_response( 23 | Request( 24 | method="POST", 25 | url=f"{event_api_url}events", 26 | json={"body": api_data}, 27 | ) 28 | ) 29 | api_response: Dict = response.json() 30 | 31 | # Then 32 | # ** we should get a 200 status code and "transaction_id" will be returned in the API response. 33 | assert response.status_code == status_code 34 | assert api_response.get("transaction_id", "Not Found") != "Not Found" 35 | -------------------------------------------------------------------------------- /tests/events/event_api_gw.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/events", 3 | "path": "/events", 4 | "httpMethod": "POST", 5 | "headers": { 6 | "Accept": "*/*", 7 | "Accept-Encoding": "application/json", 8 | "CloudFront-Forwarded-Proto": "https", 9 | "CloudFront-Is-Desktop-Viewer": "true", 10 | "CloudFront-Is-Mobile-Viewer": "false", 11 | "CloudFront-Is-SmartTV-Viewer": "false", 12 | "CloudFront-Is-Tablet-Viewer": "false", 13 | "CloudFront-Viewer-ASN": "7545", 14 | "CloudFront-Viewer-Country": "AU", 15 | "Content-Type": "application/json", 16 | "Host": "4woenc2552.execute-api.ap-southeast-2.amazonaws.com", 17 | "Postman-Token": "1dae39cf-d2de-43a0-af4d-5d535f57893f", 18 | "User-Agent": "PostmanRuntime/7.29.2", 19 | "Via": "1.1 ea851a39fcf5baed153ab72ce3a876e2.cloudfront.net (CloudFront)", 20 | "X-Amz-Cf-Id": "1q7aGzIbxbLdGyzjGzK8rK7lNNN0CBkgFaSbs6sSUNUpltuw92Jb2A==", 21 | "X-Amzn-Trace-Id": "Root=1-635f4857-10076f276d7620e56d6d026e", 22 | "X-Forwarded-For": "124.150.41.102, 130.176.108.150", 23 | "X-Forwarded-Port": "443", 24 | "X-Forwarded-Proto": "https" 25 | }, 26 | "multiValueHeaders": { 27 | "Accept": [ 28 | "*/*" 29 | ], 30 | "Accept-Encoding": [ 31 | "application/json" 32 | ], 33 | "CloudFront-Forwarded-Proto": [ 34 | "https" 35 | ], 36 | "CloudFront-Is-Desktop-Viewer": [ 37 | "true" 38 | ], 39 | "CloudFront-Is-Mobile-Viewer": [ 40 | "false" 41 | ], 42 | "CloudFront-Is-SmartTV-Viewer": [ 43 | "false" 44 | ], 45 | "CloudFront-Is-Tablet-Viewer": [ 46 | "false" 47 | ], 48 | "CloudFront-Viewer-ASN": [ 49 | "7545" 50 | ], 51 | "CloudFront-Viewer-Country": [ 52 | "AU" 53 | ], 54 | "Content-Type": [ 55 | "application/json" 56 | ], 57 | "Host": [ 58 | "4woenc2552.execute-api.ap-southeast-2.amazonaws.com" 59 | ], 60 | "Postman-Token": [ 61 | "1dae39cf-d2de-43a0-af4d-5d535f57893f" 62 | ], 63 | "User-Agent": [ 64 | "PostmanRuntime/7.29.2" 65 | ], 66 | "Via": [ 67 | "1.1 ea851a39fcf5baed153ab72ce3a876e2.cloudfront.net (CloudFront)" 68 | ], 69 | "X-Amz-Cf-Id": [ 70 | "1q7aGzIbxbLdGyzjGzK8rK7lNNN0CBkgFaSbs6sSUNUpltuw92Jb2A==" 71 | ], 72 | "X-Amzn-Trace-Id": [ 73 | "Root=1-635f4857-10076f276d7620e56d6d026e" 74 | ], 75 | "X-Forwarded-For": [ 76 | "124.150.41.102, 130.176.108.150" 77 | ], 78 | "X-Forwarded-Port": [ 79 | "443" 80 | ], 81 | "X-Forwarded-Proto": [ 82 | "https" 83 | ] 84 | }, 85 | "queryStringParameters": null, 86 | "multiValueQueryStringParameters": null, 87 | "pathParameters": null, 88 | "stageVariables": null, 89 | "requestContext": { 90 | "resourceId": "rp3msy", 91 | "resourcePath": "/events", 92 | "httpMethod": "POST", 93 | "extendedRequestId": "a2g9pH33SwMF6Og=", 94 | "requestTime": "31/Oct/2022:04:00:23 +0000", 95 | "path": "/prod/events", 96 | "accountId": "308836149415", 97 | "protocol": "HTTP/1.1", 98 | "stage": "prod", 99 | "domainPrefix": "4woenc2552", 100 | "requestTimeEpoch": 1667188823101, 101 | "requestId": "8cfe08aa-51e9-49cf-8cd6-c00d415bfa97", 102 | "identity": { 103 | "cognitoIdentityPoolId": null, 104 | "accountId": null, 105 | "cognitoIdentityId": null, 106 | "caller": null, 107 | "sourceIp": "124.150.41.102", 108 | "principalOrgId": null, 109 | "accessKey": null, 110 | "cognitoAuthenticationType": null, 111 | "cognitoAuthenticationProvider": null, 112 | "userArn": null, 113 | "userAgent": "PostmanRuntime/7.29.2", 114 | "user": null 115 | }, 116 | "domainName": "4woenc2552.execute-api.ap-southeast-2.amazonaws.com", 117 | "apiId": "4woenc2552" 118 | }, 119 | "body": "{\n \"body\": \"Hello World\"\n}", 120 | "isBase64Encoded": false 121 | } 122 | -------------------------------------------------------------------------------- /tests/events/event_processor_sqs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{\"message_id\": \"message-111-XXX\", \"body\": {\"key1\": \"value1\"}}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO...", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": {}, 14 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 15 | "eventSource": "aws:sqs", 16 | "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", 17 | "awsRegion": "us-east-2" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/integration/event_api/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_api/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.integration.event_api.infrastructure import EventApiIntegrationStack 4 | 5 | 6 | @pytest.fixture(autouse=True, scope="module") 7 | def infrastructure(): 8 | """Setup and teardown logic for E2E test infrastructure 9 | 10 | Yields 11 | ------ 12 | Dict[str, str] 13 | CloudFormation Outputs from deployed infrastructure 14 | """ 15 | stack = EventApiIntegrationStack() 16 | try: 17 | yield stack.deploy() 18 | finally: 19 | stack.delete() 20 | -------------------------------------------------------------------------------- /tests/integration/event_api/infrastructure.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import RemovalPolicy, aws_logs 2 | 3 | from services.event_api.infrastructure import EventFunctionConstruct 4 | from tests.utils.base_infrastructure import BaseInfrastructure 5 | 6 | EVENT_API_FUNCTION = "EventApiFunction" 7 | EVENT_API_FUNCTION_ARN = "EventApiFunctionArn" 8 | EVENT_STORE_BUCKET = "EventStoreBucket" 9 | EVENT_MESSAGE_QUEUE = "EventMessageQueueUrl" 10 | 11 | 12 | class EventApiIntegrationStack(BaseInfrastructure): 13 | def create_resources(self): 14 | event_function = EventFunctionConstruct(self.stack, "EventIntegrationFunction") 15 | 16 | # Set expiry on Lambda log groups so they self delete when no longer needed 17 | # 18 | aws_logs.LogGroup( 19 | self.stack, 20 | "EventAPIFunction-lg", 21 | log_group_name=f"/aws/lamdba/{event_function.function.function_name}", 22 | retention=aws_logs.RetentionDays.ONE_DAY, 23 | removal_policy=RemovalPolicy.DESTROY, 24 | ) 25 | 26 | self.add_cfn_output( 27 | name=EVENT_API_FUNCTION, 28 | value=event_function.function.function_name, 29 | arn=event_function.function.function_arn, 30 | ) 31 | self.add_cfn_output( 32 | name=EVENT_STORE_BUCKET, 33 | value=event_function.bucket.bucket_name, 34 | arn=event_function.bucket.bucket_arn, 35 | ) 36 | 37 | self.add_cfn_output( 38 | name=EVENT_MESSAGE_QUEUE, value=event_function.queue.queue_url 39 | ) 40 | -------------------------------------------------------------------------------- /tests/integration/event_api/test_event_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict 3 | 4 | import boto3 5 | import pytest 6 | from mypy_boto3_s3 import S3Client 7 | from mypy_boto3_sqs import SQSClient 8 | 9 | from tests.integration.event_api.infrastructure import ( 10 | EVENT_API_FUNCTION_ARN, 11 | EVENT_MESSAGE_QUEUE, 12 | EVENT_STORE_BUCKET, 13 | ) 14 | from tests.utils.fetcher import fetch_lambda_response, fetch_test_event 15 | 16 | 17 | @pytest.fixture() 18 | def event_api_lambda_arn(infrastructure: Dict) -> str: 19 | return infrastructure.get(EVENT_API_FUNCTION_ARN) 20 | 21 | 22 | @pytest.fixture() 23 | def event_bucket_name(infrastructure: Dict) -> str: 24 | return infrastructure.get(EVENT_STORE_BUCKET) 25 | 26 | 27 | @pytest.fixture() 28 | def event_message_queue(infrastructure: Dict) -> str: 29 | return infrastructure.get(EVENT_MESSAGE_QUEUE) 30 | 31 | 32 | def test_event_api(event_api_lambda_arn, event_bucket_name, event_message_queue): 33 | # Given 34 | transaction_id = "8cfe08aa-51e9-49cf-8cd6-c00d415bfa97" 35 | api_gw_event = fetch_test_event("event_api_gw.json") 36 | api_data = {"body": "Hello World"} 37 | 38 | # When 39 | lambda_response = fetch_lambda_response( 40 | event_api_lambda_arn, payload=json.dumps(api_gw_event) 41 | ) 42 | response = lambda_response[0] 43 | s3_client: S3Client = boto3.client("s3") 44 | sqs_client: SQSClient = boto3.client("sqs") 45 | 46 | # Raise exception oj Lambda function handler failure 47 | if response.get("FunctionError"): 48 | message = json.loads(response.get("Payload").read().decode("utf-8")) 49 | raise Exception(message) 50 | 51 | # should be able to read from the S3 bucket and get thge event body 52 | # using the requestId from the event payload 53 | event_object = s3_client.get_object( 54 | Bucket=event_bucket_name, Key=f"{transaction_id}.json" 55 | ) 56 | event_data = json.loads(event_object.get("Body").read().decode("utf-8")) 57 | 58 | # read 1 message from the Queue which will be ther esince 59 | # is a synchronous lambda call 60 | the_messages = sqs_client.receive_message( 61 | QueueUrl=event_message_queue, WaitTimeSeconds=10, MaxNumberOfMessages=1 62 | ) 63 | message_data = the_messages.get("Messages", []) 64 | if len(message_data) != 1: 65 | raise Exception("No message found in SQS") 66 | 67 | message_body = json.loads(message_data[0].get("Body", "")) 68 | 69 | # Then 70 | # ** we should read the actual data injected by the API from S3. 71 | assert event_data == api_data 72 | assert message_body.get("body") == api_data 73 | assert message_body.get("message_id") == transaction_id 74 | -------------------------------------------------------------------------------- /tests/integration/event_processor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/integration/event_processor/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_processor/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.integration.event_processor.infrastructure import ( 4 | EventProcessorIntegrationStack, 5 | ) 6 | 7 | 8 | @pytest.fixture(autouse=True, scope="module") 9 | def infrastructure(): 10 | """Setup and teardown logic for E2E test infrastructure 11 | 12 | Yields 13 | ------ 14 | Dict[str, str] 15 | CloudFormation Outputs from deployed infrastructure 16 | """ 17 | stack = EventProcessorIntegrationStack() 18 | try: 19 | yield stack.deploy() 20 | finally: 21 | stack.delete() 22 | -------------------------------------------------------------------------------- /tests/integration/event_processor/infrastructure.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import RemovalPolicy, aws_logs 2 | from aws_cdk.aws_sqs import Queue, QueueEncryption 3 | 4 | from services.event_processor.infrastructure import EventProcessorConstruct 5 | from tests.utils.base_infrastructure import BaseInfrastructure 6 | 7 | EVENT_API_FUNCTION = "EventApiFunction" 8 | EVENT_API_FUNCTION_ARN = "EventApiFunctionArn" 9 | EVENT_MESSAGE_QUEUE = "EventMessageQueueUrl" 10 | EVENT_STORE_TABLE = "EventStoreTable" 11 | EVENT_STORE_TABLE_ARN = "EventStoreTableArn" 12 | 13 | 14 | class EventProcessorIntegrationStack(BaseInfrastructure): 15 | def create_resources(self): 16 | # The EventProcessorFunction requires a Queue to be provided 17 | # It is a shared resource from the EventAPIConstruct 18 | # 19 | queue = Queue( 20 | self.stack, 21 | "MessageQueue", 22 | encryption=QueueEncryption.KMS_MANAGED, 23 | enforce_ssl=True, 24 | ) 25 | event_function = EventProcessorConstruct( 26 | self.stack, "EventProcessorFunction", queue=queue 27 | ) 28 | 29 | # Set expiry on Lambda log groups so they self delete when no longer needed 30 | # 31 | aws_logs.LogGroup( 32 | self.stack, 33 | "EventProcessorFunction-lg", 34 | log_group_name=f"/aws/lamdba/{event_function.function.function_name}", 35 | retention=aws_logs.RetentionDays.ONE_DAY, 36 | removal_policy=RemovalPolicy.DESTROY, 37 | ) 38 | 39 | self.add_cfn_output( 40 | name=EVENT_API_FUNCTION, 41 | value=event_function.function.function_name, 42 | arn=event_function.function.function_arn, 43 | ) 44 | 45 | self.add_cfn_output(name=EVENT_MESSAGE_QUEUE, value=queue.queue_url) 46 | 47 | self.add_cfn_output( 48 | name=EVENT_STORE_TABLE, 49 | value=event_function.table.table_name, 50 | arn=event_function.table.table_arn, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/integration/event_processor/test_event_processor.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict 3 | 4 | import boto3 5 | import pytest 6 | from mypy_boto3_dynamodb import DynamoDBClient 7 | 8 | from tests.integration.event_processor.infrastructure import ( 9 | EVENT_API_FUNCTION_ARN, 10 | EVENT_MESSAGE_QUEUE, 11 | EVENT_STORE_TABLE, 12 | ) 13 | from tests.utils.fetcher import fetch_lambda_response, fetch_test_event 14 | 15 | 16 | @pytest.fixture() 17 | def event_api_lambda_arn(infrastructure: Dict) -> str: 18 | return infrastructure.get(EVENT_API_FUNCTION_ARN) 19 | 20 | 21 | @pytest.fixture() 22 | def event_message_queue(infrastructure: Dict) -> str: 23 | return infrastructure.get(EVENT_MESSAGE_QUEUE) 24 | 25 | 26 | @pytest.fixture() 27 | def event_storage_table(infrastructure: Dict) -> str: 28 | return infrastructure.get(EVENT_STORE_TABLE) 29 | 30 | 31 | def test_event_api(event_storage_table, event_api_lambda_arn): 32 | # Given 33 | message_id = "message-111-XXX" 34 | content = {"key1": "value1"} 35 | sqs_event = fetch_test_event("event_processor_sqs.json") 36 | 37 | # When We pass an SQS event to Lambda 38 | # 39 | lambda_response = fetch_lambda_response( 40 | event_api_lambda_arn, payload=json.dumps(sqs_event) 41 | ) 42 | response = lambda_response[0] 43 | 44 | # Raise exception on Lambda function handler failure 45 | if response.get("FunctionError"): 46 | message = json.loads(response.get("Payload").read().decode("utf-8")) 47 | raise Exception(message) 48 | 49 | # Read item from the table to test the Integration 50 | ddb_client: DynamoDBClient = boto3.resource("dynamodb") 51 | table = ddb_client.Table(event_storage_table) 52 | table_item = table.get_item(Key={"message_id": message_id}) 53 | item = table_item.get("Item") 54 | 55 | # Then 56 | assert item.get("body") == content 57 | assert item.get("message_id") == message_id 58 | -------------------------------------------------------------------------------- /tests/unit/adapters/event_store.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from services.event_processor.runtime.adapters.ports.event_store import EventStoragePort 4 | 5 | 6 | class FakeEventStore(EventStoragePort): 7 | def __init__(self) -> None: 8 | self.message_id = None 9 | self.content = None 10 | 11 | def store_event(self, message_id: str, content: Dict[str, Any]): 12 | self.message_id = message_id 13 | self.content = content 14 | -------------------------------------------------------------------------------- /tests/unit/adapters/file_port.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from services.event_api.runtime.adapters.ports.file_port import FileStoragePort 4 | 5 | 6 | class FakeStorage(FileStoragePort): 7 | def __init__(self): 8 | super().__init__() 9 | self.read_filename = None 10 | self.save_filename = None 11 | self.save_content = None 12 | 13 | def read_file(self, filename: str) -> str: 14 | self.read_filename = filename 15 | 16 | def save_file(self, filename: str, content: str) -> None: 17 | self.save_filename = f"{filename}.json" 18 | self.save_content = json.dumps(content) 19 | -------------------------------------------------------------------------------- /tests/unit/adapters/message_port.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from services.event_api.runtime.adapters.ports.message_port import MessagePort 4 | 5 | 6 | class FakeMessagePort(MessagePort): 7 | def send_event( 8 | self, message_id: str, body: Dict[str, Any], group_id: Optional[str] 9 | ): 10 | self.message_id = message_id 11 | self.body = body 12 | self.group_id = group_id 13 | -------------------------------------------------------------------------------- /tests/unit/test_event_processor.py: -------------------------------------------------------------------------------- 1 | from services.event_processor.runtime.adapters.service import ProcessorService 2 | from tests.unit.adapters.event_store import FakeEventStore 3 | 4 | 5 | def test_event_processor(): 6 | # GIVEN 7 | message_id = "xxx-111" 8 | content = {"event": "test-event", "data": {"key1": "value1"}} 9 | fake_event_store = FakeEventStore() 10 | service: ProcessorService = ProcessorService(storage_service=fake_event_store) 11 | 12 | # WHEN 13 | service.store_event(message_id, content) 14 | 15 | # THEN 16 | assert message_id == fake_event_store.message_id 17 | assert content == fake_event_store.content 18 | -------------------------------------------------------------------------------- /tests/unit/test_event_service.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from services.event_api.runtime.adapters.service import EventService 4 | from tests.unit.adapters.file_port import FakeStorage 5 | from tests.unit.adapters.message_port import FakeMessagePort 6 | 7 | 8 | def test_event_service(): 9 | # Given 10 | transaction_id = "xxx-111" 11 | event_detail = {"event": "test-event", "data": {"key1": "value1"}} 12 | fake_storage = FakeStorage() 13 | fake_message = FakeMessagePort(resource_id="sqs_queue") 14 | event_service = EventService( 15 | file_storage=fake_storage, message_provider=fake_message 16 | ) 17 | 18 | # When 19 | result = event_service.process_event( 20 | transaction_id=transaction_id, event_detail=event_detail 21 | ) 22 | 23 | # Then 24 | assert result is True 25 | assert fake_storage.save_filename == f"{transaction_id}.json" 26 | assert fake_storage.save_content == json.dumps( 27 | {"event": "test-event", "data": {"key1": "value1"}} 28 | ) 29 | assert fake_message.message_id == transaction_id 30 | assert fake_message.body == event_detail 31 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmsles/aws-lambda-python-cdk/de9ef849a6133d2ea8ba19a73fd147144b9c37b0/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/base_infrastructure.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import subprocess 5 | import sys 6 | import textwrap 7 | from pathlib import Path 8 | from typing import Dict 9 | from uuid import uuid4 10 | 11 | import boto3 12 | from aws_cdk import App, CfnOutput, Environment, Stack 13 | from mypy_boto3_cloudformation import CloudFormationClient 14 | 15 | from tests.utils.constants import CDK_OUT_PATH, SOURCE_CODE_ROOT_PATH 16 | from tests.utils.provider import InfrastructureProvider 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class BaseInfrastructure(InfrastructureProvider): 22 | RANDOM_STACK_VALUE: str = f"{uuid4()}" 23 | 24 | def __init__(self) -> None: 25 | self.feature_path = Path( 26 | sys.modules[self.__class__.__module__].__file__ 27 | ).parent # absolute path to feature 28 | self.feature_name = self.feature_path.parts[-1].replace( 29 | "_", "-" 30 | ) # logger, tracer, event-handler, etc. 31 | self.stack_name = f"test-{self.feature_name}-{self.RANDOM_STACK_VALUE}" 32 | self.stack_outputs: Dict[str, str] = {} 33 | 34 | # NOTE: CDK stack account and region are tokens, we need to resolve earlier 35 | self.session = boto3.Session() 36 | self.cfn: CloudFormationClient = self.session.client("cloudformation") 37 | self.account_id = self.session.client("sts").get_caller_identity()["Account"] 38 | self.region = self.session.region_name 39 | 40 | self.app = App() 41 | self.stack = Stack( 42 | self.app, 43 | self.stack_name, 44 | env=Environment(account=self.account_id, region=self.region), 45 | ) 46 | 47 | # NOTE: Introspect feature details to generate CDK App (_create_temp_cdk_app method), Synth and Deployment 48 | self._feature_infra_class_name = self.__class__.__name__ 49 | self._feature_infra_module_path = self.feature_path / "infrastructure" 50 | self._feature_infra_file = self.feature_path / "infrastructure.py" 51 | self._handlers_dir = self.feature_path / "handlers" 52 | self._cdk_out_dir: Path = CDK_OUT_PATH / self.feature_name 53 | self._stack_outputs_file = self._cdk_out_dir / "stack_outputs.json" 54 | 55 | if not self._feature_infra_file.exists(): 56 | raise FileNotFoundError( 57 | "You must have your infrastructure defined in 'tests/e2e//infrastructure.py'." 58 | ) 59 | 60 | def _create_temp_cdk_app(self): 61 | """Autogenerate a CDK App with our Stack so that CDK CLI can deploy it 62 | 63 | This allows us to keep our BaseInfrastructure while supporting context lookups. 64 | """ 65 | # cdk.out/tracer/cdk_app_v39.py 66 | temp_file = self._cdk_out_dir / "cdk_app_test.py" 67 | 68 | if temp_file.exists(): 69 | # no need to regenerate CDK app since it's just boilerplate 70 | return temp_file 71 | 72 | # Convert from POSIX path to Python module: tests.e2e.tracer.infrastructure 73 | infra_module = str( 74 | self._feature_infra_module_path.relative_to(SOURCE_CODE_ROOT_PATH) 75 | ).replace(os.sep, ".") 76 | 77 | code = f""" 78 | from {infra_module} import {self._feature_infra_class_name} 79 | stack = {self._feature_infra_class_name}() 80 | stack.create_resources() 81 | stack.app.synth() 82 | """ 83 | 84 | if not self._cdk_out_dir.is_dir(): 85 | self._cdk_out_dir.mkdir(parents=True, exist_ok=True) 86 | 87 | with temp_file.open("w") as fd: 88 | fd.write(textwrap.dedent(code)) 89 | 90 | # allow CDK to read/execute file for stack deployment 91 | temp_file.chmod(0o755) 92 | return temp_file 93 | 94 | def _sync_stack_name(self, stack_output: Dict): 95 | """Synchronize initial stack name with CDK final stack name 96 | 97 | When using `cdk synth` with context methods (`from_lookup`), 98 | CDK can initialize the Stack multiple times until it resolves 99 | the context. 100 | 101 | Parameters 102 | ---------- 103 | stack_output : Dict 104 | CDK CloudFormation Outputs, where the key is the stack name 105 | """ 106 | self.stack_name = list(stack_output.keys())[0] 107 | 108 | def _read_stack_output(self): 109 | content = Path(self._stack_outputs_file).read_text() 110 | outputs: Dict = json.loads(content) 111 | self._sync_stack_name(stack_output=outputs) 112 | 113 | # discard stack_name and get outputs as dict 114 | self.stack_outputs = list(outputs.values())[0] 115 | return self.stack_outputs 116 | 117 | def create_resoures(self): 118 | pass 119 | 120 | def delete(self) -> None: 121 | """Delete CloudFormation Stack""" 122 | logger.debug(f"Deleting stack: {self.stack_name}") 123 | self.cfn.delete_stack(StackName=self.stack_name) 124 | 125 | def deploy(self) -> Dict[str, str]: 126 | """Synthesize and deploy a CDK app, and return its stack outputs 127 | 128 | NOTE: It auto-generates a temporary CDK app to benefit from CDK CLI lookup features 129 | 130 | Returns 131 | ------- 132 | Dict[str, str] 133 | CloudFormation Stack Outputs with output key and value 134 | """ 135 | stack_file = self._create_temp_cdk_app() 136 | synth_command = ( 137 | f"npx cdk synth --app 'python {stack_file}' -o {self._cdk_out_dir}" 138 | ) 139 | deploy_command = ( 140 | f"npx cdk deploy --app '{self._cdk_out_dir}' -O {self._stack_outputs_file} " 141 | "--require-approval=never --method=direct" 142 | ) 143 | 144 | # CDK launches a background task, so we must wait 145 | subprocess.check_output(synth_command, shell=True) 146 | subprocess.check_output(deploy_command, shell=True) 147 | return self._read_stack_output() 148 | 149 | def add_cfn_output(self, name: str, value: str, arn: str = ""): 150 | """Create {Name} and optionally {Name}Arn CloudFormation Outputs. 151 | 152 | Parameters 153 | ---------- 154 | name : str 155 | CloudFormation Output Key 156 | value : str 157 | CloudFormation Output Value 158 | arn : str 159 | CloudFormation Output Value for ARN 160 | """ 161 | CfnOutput(self.stack, f"{name}", value=value) 162 | if arn: 163 | CfnOutput(self.stack, f"{name}Arn", value=arn) 164 | -------------------------------------------------------------------------------- /tests/utils/constants.py: -------------------------------------------------------------------------------- 1 | from cdk.stack import CDK_PACKAGE_PATH 2 | 3 | SOURCE_CODE_ROOT_PATH = CDK_PACKAGE_PATH.parent 4 | CDK_OUT_PATH = SOURCE_CODE_ROOT_PATH / "cdk.out" 5 | -------------------------------------------------------------------------------- /tests/utils/fetcher.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from pathlib import Path 4 | from typing import Any, Optional, Tuple 5 | 6 | import boto3 7 | import requests as requests 8 | from mypy_boto3_lambda import LambdaClient 9 | from mypy_boto3_lambda.type_defs import InvocationResponseTypeDef 10 | from requests import Request, Response 11 | from requests.exceptions import RequestException 12 | from retry import retry 13 | 14 | 15 | def fetch_test_event(name: str) -> Any: 16 | path = Path(__file__).parent.parent.joinpath("events").joinpath(name) 17 | return json.loads(path.read_text()) 18 | 19 | 20 | def fetch_lambda_response( 21 | lambda_arn: str, 22 | payload: Optional[str] = None, 23 | client: Optional[LambdaClient] = None, 24 | ) -> Tuple[InvocationResponseTypeDef, datetime]: 25 | client = client or boto3.client("lambda") 26 | payload = payload or "" 27 | execution_time = datetime.utcnow() 28 | return ( 29 | client.invoke( 30 | FunctionName=lambda_arn, InvocationType="RequestResponse", Payload=payload 31 | ), 32 | execution_time, 33 | ) 34 | 35 | 36 | @retry(RequestException, delay=2, jitter=1.5, tries=5) 37 | def fetch_http_response(request: Request) -> Response: 38 | session = requests.Session() 39 | result = session.send(request.prepare()) 40 | result.raise_for_status() 41 | return result 42 | -------------------------------------------------------------------------------- /tests/utils/provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Dict 3 | 4 | 5 | class InfrastructureProvider(ABC): 6 | @abstractmethod 7 | def deploy(self) -> Dict[str, str]: 8 | pass 9 | 10 | @abstractmethod 11 | def delete(self): 12 | pass 13 | 14 | @abstractmethod 15 | def create_resources(self): 16 | pass 17 | --------------------------------------------------------------------------------