├── .flake8 ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── buildspec.yml ├── images └── lambda-to-slack.png ├── src ├── config.py ├── exceptions.py ├── handlers.py ├── lambdainit.py ├── lambdalogging.py └── slack.py ├── template.yml └── test └── unit ├── conftest.py ├── test_constants.py ├── test_handlers.py └── test_slack.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E126 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | /*.zip 138 | 139 | # PyInstaller 140 | # Usually these files are written by a python script from a template 141 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 142 | *.manifest 143 | *.spec 144 | 145 | # Installer logs 146 | pip-log.txt 147 | pip-delete-this-directory.txt 148 | 149 | # Unit test / coverage reports 150 | htmlcov/ 151 | .tox/ 152 | .coverage 153 | .coverage.* 154 | .cache 155 | .pytest_cache/ 156 | nosetests.xml 157 | coverage.xml 158 | *.cover 159 | .hypothesis/ 160 | 161 | # Translations 162 | *.mo 163 | *.pot 164 | 165 | # Flask stuff: 166 | instance/ 167 | .webassets-cache 168 | 169 | # Scrapy stuff: 170 | .scrapy 171 | 172 | # Sphinx documentation 173 | docs/_build/ 174 | 175 | # PyBuilder 176 | target/ 177 | 178 | # Jupyter Notebook 179 | .ipynb_checkpoints 180 | 181 | # pyenv 182 | .python-version 183 | 184 | # celery beat schedule file 185 | celerybeat-schedule.* 186 | 187 | # SageMath parsed files 188 | *.sage.py 189 | 190 | # Environments 191 | .env 192 | .venv 193 | env/ 194 | venv/ 195 | ENV/ 196 | env.bak/ 197 | venv.bak/ 198 | 199 | # Spyder project settings 200 | .spyderproject 201 | .spyproject 202 | 203 | # Rope project settings 204 | .ropeproject 205 | 206 | # mkdocs documentation 207 | /site 208 | 209 | # mypy 210 | .mypy_cache/ 211 | 212 | ### VisualStudioCode ### 213 | .vscode/* 214 | !.vscode/settings.json 215 | !.vscode/tasks.json 216 | !.vscode/launch.json 217 | !.vscode/extensions.json 218 | .history 219 | 220 | ### Windows ### 221 | # Windows thumbnail cache files 222 | Thumbs.db 223 | ehthumbs.db 224 | ehthumbs_vista.db 225 | 226 | # Folder config file 227 | Desktop.ini 228 | 229 | # Recycle Bin used on file shares 230 | $RECYCLE.BIN/ 231 | 232 | # Windows Installer files 233 | *.cab 234 | *.msi 235 | *.msm 236 | *.msp 237 | 238 | # Windows shortcuts 239 | *.lnk 240 | 241 | # Build folder 242 | 243 | */build/* 244 | 245 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 246 | 247 | # vim, mac stuff, powerpoint temp files (I use powerpoint for architecture diagrams) 248 | .*.sw[op] 249 | .DS_Store 250 | ~$*.ppt* 251 | 252 | # SAM CLI build dir 253 | .aws-sam 254 | 255 | # We're using pipenv so any requirements.txt files are auto-generated and should be ignored by git 256 | requirements.txt 257 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.unitTest.unittestEnabled": false, 3 | "python.unitTest.nosetestsEnabled": false, 4 | "python.unitTest.pyTestEnabled": true, 5 | "python.unitTest.pyTestPath": "pipenv", 6 | "python.unitTest.pyTestArgs": [ 7 | "run", 8 | "py.test", 9 | "--cov", 10 | "app", 11 | "--cov-fail-under", 12 | "85", 13 | "-vv", 14 | "test/unit" 15 | ], 16 | "python.linting.pylintEnabled": false, 17 | "python.linting.ignorePatterns": [ 18 | ".vscode/*.py", 19 | "**/site-packages/**/*.py", 20 | "dist/**/*.py" 21 | ], 22 | "python.linting.flake8Enabled": true, 23 | "python.linting.flake8Path": "pipenv", 24 | "python.linting.flake8Args": [ 25 | "run", 26 | "flake8", 27 | "app" 28 | ], 29 | "python.linting.pydocstyleEnabled": true, 30 | "python.linting.pydocstylePath": "pipenv", 31 | "python.linting.pydocstyleArgs": [ 32 | "run", 33 | "pydocstyle", 34 | "app" 35 | ], 36 | "python.formatting.autopep8Path": "pipenv", 37 | "python.formatting.autopep8Args": [ 38 | "run", 39 | "autopep8", 40 | "--max-line-length=120", 41 | "--ignore", 42 | "E126,E226,E24,W503" 43 | ] 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Keeton Hodgson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | PY_VERSION := 3.7 3 | 4 | export PYTHONUNBUFFERED := 1 5 | 6 | SRC_DIR := src 7 | SAM_DIR := .aws-sam 8 | 9 | # Required environment variables (user must override) 10 | 11 | # S3 bucket used for packaging SAM templates 12 | PACKAGE_BUCKET ?= your-bucket-here 13 | 14 | # user can optionally override the following by setting environment variables with the same names before running make 15 | 16 | # Path to system pip 17 | PIP ?= pip 18 | # Default AWS CLI region 19 | AWS_DEFAULT_REGION ?= us-east-1 20 | STACK_NAME ?= lambda-to-slack 21 | SLACK_URL ?= https://slack.com 22 | 23 | PYTHON := $(shell /usr/bin/which python$(PY_VERSION)) 24 | 25 | .DEFAULT_GOAL := build 26 | .PHONY: test clean undeploy deploy package compile build publish bootstrap init 27 | 28 | clean: 29 | rm -f $(SRC_DIR)/requirements.txt 30 | rm -rf $(SAM_DIR) 31 | 32 | # used once just after project creation to lock and install dependencies 33 | bootstrap: 34 | $(PYTHON) -m $(PIP) install pipenv --user 35 | pipenv lock 36 | pipenv sync --dev 37 | 38 | # used by CI build to install dependencies 39 | init: 40 | $(PYTHON) -m $(PIP) install pipenv --user 41 | pipenv sync --dev 42 | 43 | test: 44 | pipenv run flake8 $(SRC_DIR) 45 | pipenv run pydocstyle $(SRC_DIR) 46 | pipenv run cfn-lint template.yml 47 | pipenv run py.test --cov=$(SRC_DIR) --cov-fail-under=90 -vv test/unit 48 | 49 | compile: test 50 | pipenv lock --requirements > $(SRC_DIR)/requirements.txt 51 | pipenv run sam build 52 | 53 | build: compile 54 | 55 | package: compile 56 | pipenv run sam package --s3-bucket $(PACKAGE_BUCKET) --output-template-file $(SAM_DIR)/packaged-template.yml 57 | 58 | deploy: package 59 | pipenv run sam deploy --template-file $(SAM_DIR)/packaged-template.yml --stack-name $(STACK_NAME) --capabilities CAPABILITY_IAM --parameter-overrides SlackUrl=${SLACK_URL} 60 | 61 | # used to delete the cfn stack 62 | undeploy: 63 | pipenv run aws cloudformation delete-stack --stack-name $(STACK_NAME) 64 | 65 | publish: package 66 | pipenv run sam publish --template $(SAM_DIR)/packaged-template.yml 67 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | [packages] 8 | boto3 = "*" 9 | aws-xray-sdk = "*" 10 | requests = "*" 11 | 12 | [dev-packages] 13 | flake8 = "*" 14 | autopep8 = "*" 15 | pydocstyle = "*" 16 | cfn-lint = "*" 17 | aws-sam-cli = "*" 18 | awscli = "*" 19 | pytest = "*" 20 | pytest-mock = "*" 21 | pytest-cov = "*" 22 | 23 | [requires] 24 | python_version = "3.7" 25 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "cc6406b18983be5ee6c0d8ef3dc6f2ce249186ab4876714ada1f0377d5018ee7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aws-xray-sdk": { 20 | "hashes": [ 21 | "sha256:bb74e1cc2388bd29c45e2e3eb31d0416d0f53d83baafca7b72ca9c945a2e249a", 22 | "sha256:f5e43e8c7c240064415c130b6d6cf1419cb5abf75c8735470f084599171eb77c" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.3.0" 26 | }, 27 | "boto3": { 28 | "hashes": [ 29 | "sha256:31c59a141570e05acca0ed67084b19b5d77055b1d89889d816cdf0152a00ac5d", 30 | "sha256:a6aac15181ee37ffb4867d07ce3dcf932379a2d5c36b4f6648ad28a610cf51c0" 31 | ], 32 | "index": "pypi", 33 | "version": "==1.9.81" 34 | }, 35 | "botocore": { 36 | "hashes": [ 37 | "sha256:21e4e9d6fbdf583d1c6f0f3f6eb3e1624dd66b515026cfaa8522c44e2130e42e", 38 | "sha256:783cb25515f59ea191cfe70944d1c69c988911845f3e86b3559b49b52e36ea27" 39 | ], 40 | "version": "==1.12.81" 41 | }, 42 | "certifi": { 43 | "hashes": [ 44 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 45 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 46 | ], 47 | "version": "==2018.11.29" 48 | }, 49 | "chardet": { 50 | "hashes": [ 51 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 52 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 53 | ], 54 | "version": "==3.0.4" 55 | }, 56 | "docutils": { 57 | "hashes": [ 58 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 59 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 60 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 61 | ], 62 | "version": "==0.14" 63 | }, 64 | "future": { 65 | "hashes": [ 66 | "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" 67 | ], 68 | "version": "==0.17.1" 69 | }, 70 | "idna": { 71 | "hashes": [ 72 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 73 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 74 | ], 75 | "version": "==2.8" 76 | }, 77 | "jmespath": { 78 | "hashes": [ 79 | "sha256:6a81d4c9aa62caf061cb517b4d9ad1dd300374cd4706997aff9cd6aedd61fc64", 80 | "sha256:f11b4461f425740a1d908e9a3f7365c3d2e569f6ca68a2ff8bc5bcd9676edd63" 81 | ], 82 | "version": "==0.9.3" 83 | }, 84 | "jsonpickle": { 85 | "hashes": [ 86 | "sha256:8b6212f1155f43ce67fa945efae6d010ed059f3ca5ed377aa070e5903d45b722", 87 | "sha256:d43ede55b3d9b5524a8e11566ea0b11c9c8109116ef6a509a1b619d2041e7397", 88 | "sha256:ed4adf0d14564c56023862eabfac211cf01211a20c5271896c8ab6f80c68086c" 89 | ], 90 | "version": "==1.0" 91 | }, 92 | "python-dateutil": { 93 | "hashes": [ 94 | "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", 95 | "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" 96 | ], 97 | "markers": "python_version >= '2.7'", 98 | "version": "==2.7.5" 99 | }, 100 | "requests": { 101 | "hashes": [ 102 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 103 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 104 | ], 105 | "index": "pypi", 106 | "version": "==2.21.0" 107 | }, 108 | "s3transfer": { 109 | "hashes": [ 110 | "sha256:90dc18e028989c609146e241ea153250be451e05ecc0c2832565231dacdf59c1", 111 | "sha256:c7a9ec356982d5e9ab2d4b46391a7d6a950e2b04c472419f5fdec70cc0ada72f" 112 | ], 113 | "version": "==0.1.13" 114 | }, 115 | "six": { 116 | "hashes": [ 117 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 118 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 119 | ], 120 | "version": "==1.12.0" 121 | }, 122 | "urllib3": { 123 | "hashes": [ 124 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 125 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 126 | ], 127 | "markers": "python_version >= '3.4'", 128 | "version": "==1.24.1" 129 | }, 130 | "wrapt": { 131 | "hashes": [ 132 | "sha256:e03f19f64d81d0a3099518ca26b04550026f131eced2e76ced7b85c6b8d32128" 133 | ], 134 | "version": "==1.11.0" 135 | } 136 | }, 137 | "develop": { 138 | "arrow": { 139 | "hashes": [ 140 | "sha256:9cb4a910256ed536751cd5728673bfb53e6f0026e240466f90c2a92c0b79c895" 141 | ], 142 | "version": "==0.13.0" 143 | }, 144 | "atomicwrites": { 145 | "hashes": [ 146 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 147 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 148 | ], 149 | "version": "==1.2.1" 150 | }, 151 | "attrs": { 152 | "hashes": [ 153 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 154 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 155 | ], 156 | "version": "==18.2.0" 157 | }, 158 | "autopep8": { 159 | "hashes": [ 160 | "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" 161 | ], 162 | "index": "pypi", 163 | "version": "==1.4.3" 164 | }, 165 | "aws-lambda-builders": { 166 | "hashes": [ 167 | "sha256:4380dd8430f443b46867ff2b03c9673ed6042a3ffc6f05c18764d062d04c4049", 168 | "sha256:fed94fd909e19fe7b8a22c05a40228eeb6072cbbd675d07416cd5746a8df0746" 169 | ], 170 | "version": "==0.0.5" 171 | }, 172 | "aws-sam-cli": { 173 | "hashes": [ 174 | "sha256:1aa6ca976fb697dd44b4f0c293c9275587e9d160e873bc035a3ffccd14b1b16e", 175 | "sha256:523cd125bd89cd1d42559101a8500f74f88067fd9b26f72b1d05c5d00a76bed9", 176 | "sha256:e10399639096d9617dd566f5e0326fe140b117a0b8d6611d5d4404b469ef9c46" 177 | ], 178 | "index": "pypi", 179 | "version": "==0.10.0" 180 | }, 181 | "aws-sam-translator": { 182 | "hashes": [ 183 | "sha256:1334795a85077cd5741822149260f90104fb2a01699171c9e9567c0db76ed74d" 184 | ], 185 | "version": "==1.9.0" 186 | }, 187 | "awscli": { 188 | "hashes": [ 189 | "sha256:029dddd5f847d3a6145a964a494a6fc0252599b5d1150e9ce5c7af0f58d87d9a", 190 | "sha256:71cef392554d52401ac9fedafd5d4e6c21fd50d2e6112bbe35523dc731d0ad3a" 191 | ], 192 | "index": "pypi", 193 | "version": "==1.16.91" 194 | }, 195 | "binaryornot": { 196 | "hashes": [ 197 | "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", 198 | "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4" 199 | ], 200 | "version": "==0.4.4" 201 | }, 202 | "boto3": { 203 | "hashes": [ 204 | "sha256:31c59a141570e05acca0ed67084b19b5d77055b1d89889d816cdf0152a00ac5d", 205 | "sha256:a6aac15181ee37ffb4867d07ce3dcf932379a2d5c36b4f6648ad28a610cf51c0" 206 | ], 207 | "index": "pypi", 208 | "version": "==1.9.81" 209 | }, 210 | "botocore": { 211 | "hashes": [ 212 | "sha256:21e4e9d6fbdf583d1c6f0f3f6eb3e1624dd66b515026cfaa8522c44e2130e42e", 213 | "sha256:783cb25515f59ea191cfe70944d1c69c988911845f3e86b3559b49b52e36ea27" 214 | ], 215 | "version": "==1.12.81" 216 | }, 217 | "certifi": { 218 | "hashes": [ 219 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 220 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 221 | ], 222 | "version": "==2018.11.29" 223 | }, 224 | "cfn-lint": { 225 | "hashes": [ 226 | "sha256:d6a97b27a08f6bcd20ffddb9ff4dfb6fc67a175676a281b02182f68405ae9ae2", 227 | "sha256:e1632a5a81440605a1cebd08f26730077873cf57657838d4f8da696ba3e40a8e" 228 | ], 229 | "index": "pypi", 230 | "version": "==0.12.1" 231 | }, 232 | "chardet": { 233 | "hashes": [ 234 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 235 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 236 | ], 237 | "version": "==3.0.4" 238 | }, 239 | "chevron": { 240 | "hashes": [ 241 | "sha256:95b0a055ef0ada5eb061d60be64a7f70670b53372ccd221d1b88adf1c41a9094", 242 | "sha256:f95054a8b303268ebf3efd6bdfc8c1b428d3fc92327913b4e236d062ec61c989" 243 | ], 244 | "version": "==0.13.1" 245 | }, 246 | "click": { 247 | "hashes": [ 248 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 249 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 250 | ], 251 | "version": "==6.7" 252 | }, 253 | "colorama": { 254 | "hashes": [ 255 | "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", 256 | "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" 257 | ], 258 | "version": "==0.3.9" 259 | }, 260 | "cookiecutter": { 261 | "hashes": [ 262 | "sha256:1316a52e1c1f08db0c9efbf7d876dbc01463a74b155a0d83e722be88beda9a3e", 263 | "sha256:ed8f54a8fc79b6864020d773ce11539b5f08e4617f353de1f22d23226f6a0d36" 264 | ], 265 | "version": "==1.6.0" 266 | }, 267 | "coverage": { 268 | "hashes": [ 269 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 270 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 271 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 272 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 273 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 274 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 275 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 276 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 277 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 278 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 279 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 280 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 281 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 282 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 283 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 284 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 285 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 286 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 287 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 288 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 289 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 290 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 291 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 292 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 293 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 294 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 295 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 296 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 297 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 298 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 299 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 300 | ], 301 | "version": "==4.5.2" 302 | }, 303 | "dateparser": { 304 | "hashes": [ 305 | "sha256:940828183c937bcec530753211b70f673c0a9aab831e43273489b310538dff86", 306 | "sha256:b452ef8b36cd78ae86a50721794bc674aa3994e19b570f7ba92810f4e0a2ae03" 307 | ], 308 | "version": "==0.7.0" 309 | }, 310 | "docker": { 311 | "hashes": [ 312 | "sha256:2840ffb9dc3ef6d00876bde476690278ab13fa1f8ba9127ef855ac33d00c3152", 313 | "sha256:5831256da3477723362bc71a8df07b8cd8493e4a4a60cebd45580483edbe48ae" 314 | ], 315 | "version": "==3.7.0" 316 | }, 317 | "docker-pycreds": { 318 | "hashes": [ 319 | "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", 320 | "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49" 321 | ], 322 | "version": "==0.4.0" 323 | }, 324 | "docutils": { 325 | "hashes": [ 326 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 327 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 328 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 329 | ], 330 | "version": "==0.14" 331 | }, 332 | "flake8": { 333 | "hashes": [ 334 | "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", 335 | "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" 336 | ], 337 | "index": "pypi", 338 | "version": "==3.6.0" 339 | }, 340 | "flask": { 341 | "hashes": [ 342 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 343 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 344 | ], 345 | "version": "==1.0.2" 346 | }, 347 | "future": { 348 | "hashes": [ 349 | "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" 350 | ], 351 | "version": "==0.17.1" 352 | }, 353 | "idna": { 354 | "hashes": [ 355 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 356 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 357 | ], 358 | "version": "==2.8" 359 | }, 360 | "itsdangerous": { 361 | "hashes": [ 362 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 363 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 364 | ], 365 | "version": "==1.1.0" 366 | }, 367 | "jinja2": { 368 | "hashes": [ 369 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 370 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 371 | ], 372 | "version": "==2.10" 373 | }, 374 | "jinja2-time": { 375 | "hashes": [ 376 | "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", 377 | "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa" 378 | ], 379 | "version": "==0.2.0" 380 | }, 381 | "jmespath": { 382 | "hashes": [ 383 | "sha256:6a81d4c9aa62caf061cb517b4d9ad1dd300374cd4706997aff9cd6aedd61fc64", 384 | "sha256:f11b4461f425740a1d908e9a3f7365c3d2e569f6ca68a2ff8bc5bcd9676edd63" 385 | ], 386 | "version": "==0.9.3" 387 | }, 388 | "jsonpatch": { 389 | "hashes": [ 390 | "sha256:49f29cab70e9068db3b1dc6b656cbe2ee4edf7dfe9bf5a0055f17a4b6804a4b9", 391 | "sha256:8bf92fa26bc42c346c03bd4517722a8e4f429225dbe775ac774b2c70d95dbd33" 392 | ], 393 | "version": "==1.23" 394 | }, 395 | "jsonpointer": { 396 | "hashes": [ 397 | "sha256:c192ba86648e05fdae4f08a17ec25180a9aef5008d973407b581798a83975362", 398 | "sha256:ff379fa021d1b81ab539f5ec467c7745beb1a5671463f9dcc2b2d458bd361c1e" 399 | ], 400 | "version": "==2.0" 401 | }, 402 | "jsonschema": { 403 | "hashes": [ 404 | "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", 405 | "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" 406 | ], 407 | "version": "==2.6.0" 408 | }, 409 | "markupsafe": { 410 | "hashes": [ 411 | "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", 412 | "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", 413 | "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", 414 | "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", 415 | "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", 416 | "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", 417 | "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", 418 | "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", 419 | "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", 420 | "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", 421 | "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", 422 | "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", 423 | "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", 424 | "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", 425 | "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", 426 | "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", 427 | "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", 428 | "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", 429 | "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", 430 | "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", 431 | "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", 432 | "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", 433 | "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", 434 | "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", 435 | "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", 436 | "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", 437 | "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", 438 | "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" 439 | ], 440 | "version": "==1.1.0" 441 | }, 442 | "mccabe": { 443 | "hashes": [ 444 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 445 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 446 | ], 447 | "version": "==0.6.1" 448 | }, 449 | "more-itertools": { 450 | "hashes": [ 451 | "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", 452 | "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", 453 | "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" 454 | ], 455 | "version": "==5.0.0" 456 | }, 457 | "pluggy": { 458 | "hashes": [ 459 | "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", 460 | "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" 461 | ], 462 | "version": "==0.8.1" 463 | }, 464 | "poyo": { 465 | "hashes": [ 466 | "sha256:c34a5413191210ed564640510e9c4a4ba3b698746d6b454d46eb5bfb30edcd1d", 467 | "sha256:d1c317054145a6b1ca0608b5e676b943ddc3bfd671f886a2fe09288b98221edb" 468 | ], 469 | "version": "==0.4.2" 470 | }, 471 | "py": { 472 | "hashes": [ 473 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 474 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 475 | ], 476 | "version": "==1.7.0" 477 | }, 478 | "pyasn1": { 479 | "hashes": [ 480 | "sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", 481 | "sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e" 482 | ], 483 | "version": "==0.4.5" 484 | }, 485 | "pycodestyle": { 486 | "hashes": [ 487 | "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", 488 | "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" 489 | ], 490 | "version": "==2.4.0" 491 | }, 492 | "pydocstyle": { 493 | "hashes": [ 494 | "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", 495 | "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", 496 | "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" 497 | ], 498 | "index": "pypi", 499 | "version": "==3.0.0" 500 | }, 501 | "pyflakes": { 502 | "hashes": [ 503 | "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", 504 | "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" 505 | ], 506 | "version": "==2.0.0" 507 | }, 508 | "pytest": { 509 | "hashes": [ 510 | "sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2", 511 | "sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be" 512 | ], 513 | "index": "pypi", 514 | "version": "==4.1.1" 515 | }, 516 | "pytest-cov": { 517 | "hashes": [ 518 | "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", 519 | "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" 520 | ], 521 | "index": "pypi", 522 | "version": "==2.6.1" 523 | }, 524 | "pytest-mock": { 525 | "hashes": [ 526 | "sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928", 527 | "sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0" 528 | ], 529 | "index": "pypi", 530 | "version": "==1.10.0" 531 | }, 532 | "python-dateutil": { 533 | "hashes": [ 534 | "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", 535 | "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" 536 | ], 537 | "markers": "python_version >= '2.7'", 538 | "version": "==2.7.5" 539 | }, 540 | "pytz": { 541 | "hashes": [ 542 | "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", 543 | "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" 544 | ], 545 | "version": "==2018.9" 546 | }, 547 | "pyyaml": { 548 | "hashes": [ 549 | "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", 550 | "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", 551 | "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", 552 | "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", 553 | "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", 554 | "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", 555 | "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", 556 | "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", 557 | "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", 558 | "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", 559 | "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" 560 | ], 561 | "version": "==3.13" 562 | }, 563 | "regex": { 564 | "hashes": [ 565 | "sha256:15b4a185ae9782133f398f8ab7c29612a6e5f34ea9411e4cd36e91e78c347ebe", 566 | "sha256:3852b76f0b6d7bd98d328d548716c151b79017f2b81347360f26e5db10fb6503", 567 | "sha256:79a6a60ed1ee3b12eb0e828c01d75e3b743af6616d69add6c2fde1d425a4ba3f", 568 | "sha256:a2938c290b3be2c7cadafa21de3051f2ed23bfaf88728a1fe5dc552cbfdb0326", 569 | "sha256:aff7414712c9e6d260609da9c9af3aacebfbc307a4abe3376c7736e2a6c8563f", 570 | "sha256:d03782f0b0fa34f8f1dbdc94e27cf193b83c6105307a8c10563938c6d85180d9", 571 | "sha256:db79ac3d81e655dc12d38a865dd6d1b569a28fab4c53749051cd599a6eb7614f", 572 | "sha256:e803b3646c3f9c47f1f3dc870173c5d79c0fd2fd8e40bf917b97c7b56701baff", 573 | "sha256:e9660ccca360b6bd79606aab3672562ebb14bce6af6c501107364668543f4bef" 574 | ], 575 | "version": "==2018.11.22" 576 | }, 577 | "requests": { 578 | "hashes": [ 579 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 580 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 581 | ], 582 | "index": "pypi", 583 | "version": "==2.21.0" 584 | }, 585 | "rsa": { 586 | "hashes": [ 587 | "sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5", 588 | "sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd" 589 | ], 590 | "version": "==3.4.2" 591 | }, 592 | "s3transfer": { 593 | "hashes": [ 594 | "sha256:90dc18e028989c609146e241ea153250be451e05ecc0c2832565231dacdf59c1", 595 | "sha256:c7a9ec356982d5e9ab2d4b46391a7d6a950e2b04c472419f5fdec70cc0ada72f" 596 | ], 597 | "version": "==0.1.13" 598 | }, 599 | "serverlessrepo": { 600 | "hashes": [ 601 | "sha256:8d6d5f3431d98fe114e2ea5dbbb2f77e5bc8c9cc3401843eeac7734778aced62", 602 | "sha256:e32a6f55f55a36ae17cf80e45f75ca8f7b23498cbcdc60599efce9955c9ab879" 603 | ], 604 | "version": "==0.1.5" 605 | }, 606 | "six": { 607 | "hashes": [ 608 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 609 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 610 | ], 611 | "version": "==1.12.0" 612 | }, 613 | "snowballstemmer": { 614 | "hashes": [ 615 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", 616 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" 617 | ], 618 | "version": "==1.2.1" 619 | }, 620 | "tzlocal": { 621 | "hashes": [ 622 | "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e" 623 | ], 624 | "version": "==1.5.1" 625 | }, 626 | "urllib3": { 627 | "hashes": [ 628 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 629 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 630 | ], 631 | "markers": "python_version >= '3.4'", 632 | "version": "==1.24.1" 633 | }, 634 | "websocket-client": { 635 | "hashes": [ 636 | "sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786", 637 | "sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849" 638 | ], 639 | "version": "==0.54.0" 640 | }, 641 | "werkzeug": { 642 | "hashes": [ 643 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 644 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 645 | ], 646 | "version": "==0.14.1" 647 | }, 648 | "wheel": { 649 | "hashes": [ 650 | "sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6", 651 | "sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44" 652 | ], 653 | "version": "==0.32.3" 654 | }, 655 | "whichcraft": { 656 | "hashes": [ 657 | "sha256:7533870f751901a0ce43c93cc9850186e9eba7fe58c924dfb435968ba9c9fa4e", 658 | "sha256:fecddd531f237ffc5db8b215409afb18fa30300699064cca4817521b4fc81815" 659 | ], 660 | "version": "==0.5.2" 661 | } 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/badge/Available-serverless%20app%20repository-blue.svg)](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:289559741701:applications~lambda-to-slack) 2 | 3 | # lambda-to-slack 4 | 5 | This serverless app posts messages to Slack. 6 | 7 | ## App Architecture 8 | 9 | ![App Architecture](https://github.com/keetonian/lambda-to-slack/raw/master/images/lambda-to-slack.png) 10 | 11 | ## Installation Instructions 12 | 13 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login 14 | 1. Go to the app's page on the [Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:289559741701:applications~lambda-to-slack) and click "Deploy" 15 | 1. Provide the required app parameters (see parameter details below) and click "Deploy" 16 | 17 | ## Using this Application 18 | 19 | This lambda function expects to be called with a JSON array of strings, and will post each string to a Slack channel via a webhook. 20 | 21 | ### Slack Url 22 | To get a webhook URL for this application: 23 | * Navigate to https://api.slack.com 24 | * Click on the "Start Building" button 25 | * Give your app a name and select a workspace 26 | * Under "Add features and functionality" select "Incoming Webhooks" 27 | * Turn on "Incoming Webhooks" and click "Add New Webhook to Workspace" 28 | * Select the desired channel and click "Authorize" 29 | * Copy the generated Webhook URL 30 | 31 | ## App Parameters 32 | 33 | 1. `SlackUrl` (required) - Webhook URL for integration with Slack 34 | 1. `LogLevel` (optional) - Log level for Lambda function logging, e.g., ERROR, INFO, DEBUG, etc. Default: INFO 35 | 36 | ## App Outputs 37 | 38 | 1. `LambdaToSlackName` - Lambda function name. 39 | 1. `LambdaToSlackArn` - Lambda function ARN. 40 | 41 | ## License Summary 42 | 43 | This code is made available under the MIT license. See the LICENSE file. 44 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | commands: 5 | - make init 6 | build: 7 | commands: 8 | - make package 9 | artifacts: 10 | files: 11 | - .aws-sam/packaged-template.yml 12 | discard-paths: yes 13 | -------------------------------------------------------------------------------- /images/lambda-to-slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keetonian/lambda-to-slack/ef5d94abf6671cfe971a6fc7f552c980e75de1ff/images/lambda-to-slack.png -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | """Environment configuration values used by lambda functions.""" 2 | 3 | import os 4 | 5 | LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') 6 | SLACK_URL = os.getenv('SLACK_URL') 7 | -------------------------------------------------------------------------------- /src/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for this application.""" 2 | 3 | 4 | class Error(Exception): 5 | """Base class for exceptions.""" 6 | 7 | pass 8 | 9 | 10 | class InputError(Error): 11 | """Exception for errors in the input.""" 12 | 13 | def __init__(self, expression, message): 14 | """Initialize the InputError.""" 15 | self.expression = expression 16 | self.message = message 17 | -------------------------------------------------------------------------------- /src/handlers.py: -------------------------------------------------------------------------------- 1 | """Lambda function handler.""" 2 | 3 | # must be the first import in files with lambda function handlers 4 | import lambdainit # noqa: F401 5 | 6 | import config 7 | import lambdalogging 8 | import slack 9 | from exceptions import InputError 10 | 11 | LOG = lambdalogging.getLogger(__name__) 12 | 13 | 14 | def post_to_slack(event, context): 15 | """Lambda function handler.""" 16 | LOG.info('Received event: %s', event) 17 | 18 | if not isinstance(event, list): 19 | raise InputError(event, "Input needs to be a json array") 20 | 21 | for message in event: 22 | slack.post_message(config.SLACK_URL, message) 23 | -------------------------------------------------------------------------------- /src/lambdainit.py: -------------------------------------------------------------------------------- 1 | """Special initializations for Lambda functions. 2 | 3 | This file must be imported as the first import in any file containing a Lambda function handler method. 4 | """ 5 | 6 | import sys 7 | 8 | # add packaged dependencies to search path 9 | sys.path.append('lib') 10 | 11 | # imports of library dependencies must come after setting up the dependency search path 12 | from aws_xray_sdk.core import patch_all # noqa: E402 13 | 14 | # patch all supported libraries for X-Ray tracing 15 | patch_all() 16 | -------------------------------------------------------------------------------- /src/lambdalogging.py: -------------------------------------------------------------------------------- 1 | """Lambda logging helper. 2 | 3 | Returns a Logger with log level set based on env variables. 4 | """ 5 | 6 | import logging 7 | 8 | import config 9 | 10 | # translate log level from string to numeric value 11 | LOG_LEVEL = getattr(logging, config.LOG_LEVEL) if hasattr(logging, config.LOG_LEVEL) else logging.INFO 12 | 13 | 14 | def getLogger(name): 15 | """Return a logger configured based on env variables.""" 16 | logger = logging.getLogger(name) 17 | # in lambda environment, logging config has already been setup so can't use logging.basicConfig to change log level 18 | logger.setLevel(LOG_LEVEL) 19 | return logger 20 | -------------------------------------------------------------------------------- /src/slack.py: -------------------------------------------------------------------------------- 1 | """Functions for interacting with slack.""" 2 | import json 3 | import requests 4 | 5 | import lambdalogging 6 | 7 | LOG = lambdalogging.getLogger(__name__) 8 | JSON_HEADER = {'Content-Type': 'application/json'} 9 | 10 | 11 | def post_message(url, message): 12 | """Post a message to slack using a webhook url.""" 13 | data = {'text': message} 14 | response = requests.post(url, data=json.dumps(data), headers=JSON_HEADER) 15 | LOG.info('Sent message: %s\nUrl: %s\nResponse: %s', message, url, response) 16 | return response.status_code 17 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Metadata: 5 | AWS::ServerlessRepo::Application: 6 | Name: lambda-to-slack 7 | Description: Posts messages to a Slack channel. 8 | Author: Keeton Hodgson 9 | SpdxLicenseId: MIT 10 | # paths are relative to .aws-sam/build directory 11 | LicenseUrl: ../../LICENSE 12 | ReadmeUrl: ../../README.md 13 | Labels: [serverless,slack,lambda] 14 | HomePageUrl: https://github.com/keetonian/lambda-to-slack 15 | # Update the semantic version and run sam publish to publish a new version of your app 16 | SemanticVersion: 0.0.1 17 | # best practice is to use git tags for each release and link to the version tag as your source code URL 18 | SourceCodeUrl: https://github.com/keetonian/lambda-to-slack/tree/0.0.1 19 | 20 | Parameters: 21 | LogLevel: 22 | Type: String 23 | Description: Log level for Lambda function logging, e.g., ERROR, INFO, DEBUG, etc 24 | Default: INFO 25 | SlackUrl: 26 | Description: Webhook URL for integration with Slack 27 | Type: String 28 | 29 | Globals: 30 | Function: 31 | Runtime: python3.7 32 | Tracing: Active 33 | Timeout: 60 34 | Environment: 35 | Variables: 36 | LOG_LEVEL: !Ref LogLevel 37 | 38 | Resources: 39 | LambdaToSlack: 40 | Type: AWS::Serverless::Function 41 | Properties: 42 | CodeUri: src/ 43 | Handler: handlers.post_to_slack 44 | Environment: 45 | Variables: 46 | SLACK_URL: !Ref SlackUrl 47 | 48 | Outputs: 49 | LambdaToSlackName: 50 | Description: "Lambda Function Name" 51 | Value: !Ref LambdaToSlack 52 | LambdaToSlackArn: 53 | Description: "Lambda Function ARN" 54 | Value: !GetAtt LambdaToSlack.Arn 55 | -------------------------------------------------------------------------------- /test/unit/conftest.py: -------------------------------------------------------------------------------- 1 | """Setup unit test environment.""" 2 | 3 | import sys 4 | import os 5 | 6 | import test_constants 7 | 8 | # make sure tests can import the app code 9 | my_path = os.path.dirname(os.path.abspath(__file__)) 10 | sys.path.insert(0, my_path + '/../../src/') 11 | 12 | # set expected config environment variables to test constants 13 | os.environ['SLACK_URL'] = test_constants.SLACK_URL 14 | -------------------------------------------------------------------------------- /test/unit/test_constants.py: -------------------------------------------------------------------------------- 1 | """Constants used for unit tests. 2 | 3 | This can be used to define values for environment variables so unit tests can use these to assert on expected values. 4 | """ 5 | 6 | SLACK_URL = 'url' 7 | 8 | EVENT = [ 9 | "message1", 10 | "message2", 11 | "message3" 12 | ] 13 | 14 | INVALID_EVENT = { 15 | "message": "body" 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/test_handlers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import handlers 3 | import requests 4 | import slack 5 | from exceptions import InputError 6 | 7 | import test_constants 8 | 9 | 10 | def test_post_to_slack(mocker): 11 | mocker.patch.object(slack, 'post_message') 12 | handlers.post_to_slack(test_constants.EVENT, None) 13 | slack.post_message.assert_any_call(test_constants.SLACK_URL, test_constants.EVENT[0]) 14 | slack.post_message.assert_any_call(test_constants.SLACK_URL, test_constants.EVENT[1]) 15 | slack.post_message.assert_any_call(test_constants.SLACK_URL, test_constants.EVENT[2]) 16 | 17 | def test_post_to_slack_input_error(mocker): 18 | mocker.patch.object(slack, 'post_message') 19 | with pytest.raises(InputError): 20 | handlers.post_to_slack(test_constants.INVALID_EVENT, None) 21 | -------------------------------------------------------------------------------- /test/unit/test_slack.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import slack 3 | import requests 4 | import pytest 5 | import test_constants 6 | 7 | 8 | def test_publish_message(mocker): 9 | mocker.patch.object(requests, 'post') 10 | response = namedtuple('response', 'status_code') 11 | response.status_code = 200 12 | requests.post.return_value = response 13 | slack.post_message(test_constants.SLACK_URL, 'message') --------------------------------------------------------------------------------