├── .env ├── .flake8 ├── .gitignore ├── .vscode └── settings.json ├── DEVELOPMENT.md ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── bin └── requirements.py ├── buildspec.yml ├── images ├── app-architecture.png ├── app-architecture.pptx └── screenshot.png ├── src ├── build.py ├── config.py ├── getbuildlogs.py ├── github_proxy.py ├── lambdainit.py ├── lambdalogging.py ├── processbuildevents.py └── s3link.py ├── template.yml └── test └── unit ├── conftest.py ├── test_build.py ├── test_constants.py ├── test_getbuildlogs.py ├── test_github_proxy.py ├── test_processbuildevents.py └── test_s3link.py /.env: -------------------------------------------------------------------------------- 1 | LAMBDA_TASK_ROOT=false 2 | -------------------------------------------------------------------------------- /.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.testing.unittestEnabled": false, 3 | "python.testing.nosetestsEnabled": false, 4 | "python.testing.pytestEnabled": true, 5 | "python.testing.pytestPath": "pipenv", 6 | "python.testing.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 | } -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Commands 4 | 5 | 1. `pipenv update --dev` - Updates dependencies, including dev dependencies. 6 | 1. `make` - lints template, runs unit tests, prepares for packaging. 7 | 1. `PACKAGE_BUCKET= PROFILE= make package` - builds and packages template for deployment. 8 | 1. `PACKAGE_BUCKET= PROFILE= make publish` - builds, packages, and publishes to Serverless Application Repository. 9 | 1. **Important:** Prior to publishing, you should test the app via the `github-codebuild-logs-test` repo (see README in that repo). 10 | 11 | ## Releases 12 | 13 | To release a new version: 14 | 15 | 1. Bump version by updating README and template (2 places in the template). Example: [https://github.com/jlhood/github-codebuild-logs/pull/30](https://github.com/jlhood/github-codebuild-logs/pull/30). 16 | 1. Tag new version commit `git tag ` 17 | 1. Push tag `git push --tags` 18 | 1. Publish (see make command above) 19 | 1. Go to [Releases](https://github.com/jlhood/github-codebuild-logs/releases) page, add release notes for new tag. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James Hood 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.12 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 | PROFILE ?= default 14 | 15 | # user can optionally override the following by setting environment variables with the same names before running make 16 | 17 | # Path to system pip 18 | PIP ?= pip 19 | # Default AWS CLI region 20 | AWS_DEFAULT_REGION ?= us-east-1 21 | 22 | PYTHON := $(shell /usr/bin/which python$(PY_VERSION)) 23 | 24 | .DEFAULT_GOAL := build 25 | 26 | clean: 27 | rm -f $(SRC_DIR)/requirements.txt 28 | rm -rf $(SAM_DIR) 29 | 30 | # used once just after project creation to lock and install dependencies 31 | bootstrap: 32 | $(PYTHON) -m $(PIP) install pipenv --user 33 | pipenv lock 34 | pipenv sync --dev 35 | 36 | # used by CI build to install dependencies 37 | init: 38 | $(PYTHON) -m $(PIP) install pipenv --user 39 | pipenv sync --dev 40 | 41 | compile: 42 | pipenv run flake8 $(SRC_DIR) 43 | pipenv run pydocstyle $(SRC_DIR) 44 | pipenv run cfn-lint template.yml 45 | pipenv run py.test --cov=$(SRC_DIR) --cov-fail-under=85 -l -vv test/unit 46 | pipenv run python bin/requirements.py > $(SRC_DIR)/requirements.txt 47 | sam build 48 | 49 | build: compile 50 | 51 | package: compile 52 | sam package --profile $(PROFILE) --s3-bucket $(PACKAGE_BUCKET) --output-template-file $(SAM_DIR)/packaged-template.yml 53 | 54 | publish: package 55 | sam publish --profile $(PROFILE) --template $(SAM_DIR)/packaged-template.yml 56 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | [packages] 8 | boto3 = "*" 9 | PyGithub = "*" 10 | aws-xray-sdk = "*" 11 | 12 | [dev-packages] 13 | flake8 = "*" 14 | autopep8 = "*" 15 | pydocstyle = "*" 16 | cfn-lint = "*" 17 | awscli = "*" 18 | pytest = "*" 19 | pytest-mock = "*" 20 | pytest-cov = "*" 21 | 22 | [requires] 23 | python_version = "3.12" 24 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "08a9ebef4a3691ead51cefff4d9b2efaa33d3a97d28b0dc23f24a568223facf5" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 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:816186126917bc35ae4e6e2f304702a43d494ecef34a39e6330f5018bdecc9f5", 22 | "sha256:d18604a8688b4bed03ce4a858cc9acd72b71400e085bf7512fc31cf657ca85f9" 23 | ], 24 | "index": "pypi", 25 | "markers": "python_version >= '3.7'", 26 | "version": "==2.13.0" 27 | }, 28 | "boto3": { 29 | "hashes": [ 30 | "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9", 31 | "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365" 32 | ], 33 | "index": "pypi", 34 | "markers": "python_version >= '3.8'", 35 | "version": "==1.34.103" 36 | }, 37 | "botocore": { 38 | "hashes": [ 39 | "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431", 40 | "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939" 41 | ], 42 | "markers": "python_version >= '3.8'", 43 | "version": "==1.34.103" 44 | }, 45 | "certifi": { 46 | "hashes": [ 47 | "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", 48 | "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" 49 | ], 50 | "markers": "python_version >= '3.6'", 51 | "version": "==2024.2.2" 52 | }, 53 | "cffi": { 54 | "hashes": [ 55 | "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", 56 | "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", 57 | "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", 58 | "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", 59 | "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", 60 | "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", 61 | "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", 62 | "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", 63 | "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", 64 | "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", 65 | "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", 66 | "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", 67 | "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", 68 | "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", 69 | "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", 70 | "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", 71 | "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", 72 | "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", 73 | "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", 74 | "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", 75 | "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", 76 | "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", 77 | "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", 78 | "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", 79 | "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", 80 | "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", 81 | "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", 82 | "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", 83 | "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", 84 | "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", 85 | "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", 86 | "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", 87 | "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", 88 | "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", 89 | "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", 90 | "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", 91 | "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", 92 | "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", 93 | "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", 94 | "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", 95 | "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", 96 | "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", 97 | "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", 98 | "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", 99 | "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", 100 | "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", 101 | "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", 102 | "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", 103 | "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", 104 | "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", 105 | "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", 106 | "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" 107 | ], 108 | "markers": "python_version >= '3.8'", 109 | "version": "==1.16.0" 110 | }, 111 | "charset-normalizer": { 112 | "hashes": [ 113 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 114 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 115 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 116 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 117 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 118 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 119 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 120 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 121 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 122 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 123 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 124 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 125 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 126 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 127 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 128 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 129 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 130 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 131 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 132 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 133 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 134 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 135 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 136 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 137 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 138 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 139 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 140 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 141 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 142 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 143 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 144 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 145 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 146 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 147 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 148 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 149 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 150 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 151 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 152 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 153 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 154 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 155 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 156 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 157 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 158 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 159 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 160 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 161 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 162 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 163 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 164 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 165 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 166 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 167 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 168 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 169 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 170 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 171 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 172 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 173 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 174 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 175 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 176 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 177 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 178 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 179 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 180 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 181 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 182 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 183 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 184 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 185 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 186 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 187 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 188 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 189 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 190 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 191 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 192 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 193 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 194 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 195 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 196 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 197 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 198 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 199 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 200 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 201 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 202 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 203 | ], 204 | "markers": "python_full_version >= '3.7.0'", 205 | "version": "==3.3.2" 206 | }, 207 | "cryptography": { 208 | "hashes": [ 209 | "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", 210 | "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", 211 | "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", 212 | "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", 213 | "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", 214 | "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", 215 | "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", 216 | "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", 217 | "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", 218 | "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", 219 | "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", 220 | "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", 221 | "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", 222 | "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", 223 | "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", 224 | "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", 225 | "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", 226 | "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", 227 | "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", 228 | "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", 229 | "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", 230 | "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", 231 | "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", 232 | "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", 233 | "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", 234 | "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", 235 | "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", 236 | "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", 237 | "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", 238 | "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", 239 | "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", 240 | "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" 241 | ], 242 | "version": "==42.0.7" 243 | }, 244 | "deprecated": { 245 | "hashes": [ 246 | "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", 247 | "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" 248 | ], 249 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 250 | "version": "==1.2.14" 251 | }, 252 | "idna": { 253 | "hashes": [ 254 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 255 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 256 | ], 257 | "markers": "python_version >= '3.5'", 258 | "version": "==3.7" 259 | }, 260 | "jmespath": { 261 | "hashes": [ 262 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 263 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 264 | ], 265 | "markers": "python_version >= '3.7'", 266 | "version": "==1.0.1" 267 | }, 268 | "pycparser": { 269 | "hashes": [ 270 | "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", 271 | "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" 272 | ], 273 | "markers": "python_version >= '3.8'", 274 | "version": "==2.22" 275 | }, 276 | "pygithub": { 277 | "hashes": [ 278 | "sha256:0148d7347a1cdeed99af905077010aef81a4dad988b0ba51d4108bf66b443f7e", 279 | "sha256:65b499728be3ce7b0cd2cd760da3b32f0f4d7bc55e5e0677617f90f6564e793e" 280 | ], 281 | "index": "pypi", 282 | "markers": "python_version >= '3.7'", 283 | "version": "==2.3.0" 284 | }, 285 | "pyjwt": { 286 | "extras": [ 287 | "crypto" 288 | ], 289 | "hashes": [ 290 | "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", 291 | "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" 292 | ], 293 | "markers": "python_version >= '3.7'", 294 | "version": "==2.8.0" 295 | }, 296 | "pynacl": { 297 | "hashes": [ 298 | "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", 299 | "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", 300 | "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", 301 | "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", 302 | "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", 303 | "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", 304 | "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", 305 | "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", 306 | "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", 307 | "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" 308 | ], 309 | "markers": "python_version >= '3.6'", 310 | "version": "==1.5.0" 311 | }, 312 | "python-dateutil": { 313 | "hashes": [ 314 | "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", 315 | "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" 316 | ], 317 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 318 | "version": "==2.9.0.post0" 319 | }, 320 | "requests": { 321 | "hashes": [ 322 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 323 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 324 | ], 325 | "markers": "python_version >= '3.7'", 326 | "version": "==2.31.0" 327 | }, 328 | "s3transfer": { 329 | "hashes": [ 330 | "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", 331 | "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" 332 | ], 333 | "markers": "python_version >= '3.8'", 334 | "version": "==0.10.1" 335 | }, 336 | "six": { 337 | "hashes": [ 338 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 339 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 340 | ], 341 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 342 | "version": "==1.16.0" 343 | }, 344 | "typing-extensions": { 345 | "hashes": [ 346 | "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", 347 | "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" 348 | ], 349 | "markers": "python_version >= '3.8'", 350 | "version": "==4.11.0" 351 | }, 352 | "urllib3": { 353 | "hashes": [ 354 | "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", 355 | "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" 356 | ], 357 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 358 | "version": "==1.26.18" 359 | }, 360 | "wrapt": { 361 | "hashes": [ 362 | "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", 363 | "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", 364 | "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", 365 | "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", 366 | "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", 367 | "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", 368 | "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", 369 | "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", 370 | "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", 371 | "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", 372 | "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", 373 | "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", 374 | "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", 375 | "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", 376 | "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", 377 | "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", 378 | "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", 379 | "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", 380 | "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", 381 | "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", 382 | "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", 383 | "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", 384 | "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", 385 | "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", 386 | "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", 387 | "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", 388 | "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", 389 | "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", 390 | "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", 391 | "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", 392 | "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", 393 | "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", 394 | "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", 395 | "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", 396 | "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", 397 | "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", 398 | "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", 399 | "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", 400 | "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", 401 | "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", 402 | "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", 403 | "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", 404 | "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", 405 | "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", 406 | "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", 407 | "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", 408 | "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", 409 | "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", 410 | "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", 411 | "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", 412 | "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", 413 | "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", 414 | "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", 415 | "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", 416 | "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", 417 | "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", 418 | "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", 419 | "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", 420 | "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", 421 | "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", 422 | "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", 423 | "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", 424 | "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", 425 | "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", 426 | "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", 427 | "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", 428 | "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", 429 | "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", 430 | "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", 431 | "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" 432 | ], 433 | "markers": "python_version >= '3.6'", 434 | "version": "==1.16.0" 435 | } 436 | }, 437 | "develop": { 438 | "annotated-types": { 439 | "hashes": [ 440 | "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", 441 | "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" 442 | ], 443 | "markers": "python_version >= '3.8'", 444 | "version": "==0.6.0" 445 | }, 446 | "attrs": { 447 | "hashes": [ 448 | "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", 449 | "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" 450 | ], 451 | "markers": "python_version >= '3.7'", 452 | "version": "==23.2.0" 453 | }, 454 | "autopep8": { 455 | "hashes": [ 456 | "sha256:1fa8964e4618929488f4ec36795c7ff12924a68b8bf01366c094fc52f770b6e7", 457 | "sha256:2bb76888c5edbcafe6aabab3c47ba534f5a2c2d245c2eddced4a30c4b4946357" 458 | ], 459 | "index": "pypi", 460 | "markers": "python_version >= '3.8'", 461 | "version": "==2.1.0" 462 | }, 463 | "aws-sam-translator": { 464 | "hashes": [ 465 | "sha256:aa93d498d8de3fb3d485c316155b1628144b823bbc176099a20de06df666fcac", 466 | "sha256:e77c65f3488566122277accd44a0f1ec018e37403e0d5fe25120d96e537e91a7" 467 | ], 468 | "markers": "python_version >= '3.8' and python_version != '4.0' and python_version <= '4.0'", 469 | "version": "==1.88.0" 470 | }, 471 | "awscli": { 472 | "hashes": [ 473 | "sha256:443b6c10c93a6f2e632d3e9572d803c1ce015388c457d3f5ca198e0c265f71d1", 474 | "sha256:9edbcd613d8dd858ca951a412fe0e28ac5d6b8ea538601963b9d57c8d11b5b10" 475 | ], 476 | "index": "pypi", 477 | "markers": "python_version >= '3.8'", 478 | "version": "==1.32.103" 479 | }, 480 | "boto3": { 481 | "hashes": [ 482 | "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9", 483 | "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365" 484 | ], 485 | "index": "pypi", 486 | "markers": "python_version >= '3.8'", 487 | "version": "==1.34.103" 488 | }, 489 | "botocore": { 490 | "hashes": [ 491 | "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431", 492 | "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939" 493 | ], 494 | "markers": "python_version >= '3.8'", 495 | "version": "==1.34.103" 496 | }, 497 | "cfn-lint": { 498 | "hashes": [ 499 | "sha256:00d47406841899c05ab6a0708df3f4e32bd7462be2097c10371d744c0050775e", 500 | "sha256:773ba1d2f232ffdbe1197cc6ce61ddbf0da1781925e9f4dde4c91b7fcd54cc80" 501 | ], 502 | "index": "pypi", 503 | "markers": "python_version >= '3.8' and python_version != '4.0' and python_version <= '4.0'", 504 | "version": "==0.87.2" 505 | }, 506 | "colorama": { 507 | "hashes": [ 508 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 509 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 510 | ], 511 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 512 | "version": "==0.4.6" 513 | }, 514 | "coverage": { 515 | "extras": [ 516 | "toml" 517 | ], 518 | "hashes": [ 519 | "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de", 520 | "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661", 521 | "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26", 522 | "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41", 523 | "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d", 524 | "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981", 525 | "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2", 526 | "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34", 527 | "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f", 528 | "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a", 529 | "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35", 530 | "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223", 531 | "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1", 532 | "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746", 533 | "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90", 534 | "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", 535 | "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca", 536 | "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8", 537 | "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596", 538 | "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e", 539 | "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd", 540 | "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e", 541 | "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3", 542 | "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e", 543 | "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312", 544 | "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7", 545 | "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572", 546 | "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428", 547 | "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f", 548 | "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07", 549 | "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e", 550 | "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4", 551 | "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136", 552 | "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5", 553 | "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8", 554 | "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d", 555 | "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228", 556 | "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206", 557 | "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa", 558 | "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e", 559 | "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be", 560 | "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5", 561 | "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668", 562 | "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601", 563 | "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057", 564 | "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146", 565 | "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f", 566 | "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8", 567 | "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7", 568 | "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987", 569 | "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19", 570 | "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece" 571 | ], 572 | "markers": "python_version >= '3.8'", 573 | "version": "==7.5.1" 574 | }, 575 | "docutils": { 576 | "hashes": [ 577 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 578 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 579 | ], 580 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 581 | "version": "==0.16" 582 | }, 583 | "exceptiongroup": { 584 | "hashes": [ 585 | "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", 586 | "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" 587 | ], 588 | "markers": "python_version < '3.11'", 589 | "version": "==1.2.1" 590 | }, 591 | "flake8": { 592 | "hashes": [ 593 | "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", 594 | "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" 595 | ], 596 | "index": "pypi", 597 | "markers": "python_full_version >= '3.8.1'", 598 | "version": "==7.0.0" 599 | }, 600 | "importlib-resources": { 601 | "hashes": [ 602 | "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c", 603 | "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145" 604 | ], 605 | "markers": "python_version < '3.9'", 606 | "version": "==6.4.0" 607 | }, 608 | "iniconfig": { 609 | "hashes": [ 610 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 611 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 612 | ], 613 | "markers": "python_version >= '3.7'", 614 | "version": "==2.0.0" 615 | }, 616 | "jmespath": { 617 | "hashes": [ 618 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 619 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 620 | ], 621 | "markers": "python_version >= '3.7'", 622 | "version": "==1.0.1" 623 | }, 624 | "jschema-to-python": { 625 | "hashes": [ 626 | "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91", 627 | "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05" 628 | ], 629 | "markers": "python_version >= '2.7'", 630 | "version": "==1.2.3" 631 | }, 632 | "jsonpatch": { 633 | "hashes": [ 634 | "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", 635 | "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c" 636 | ], 637 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 638 | "version": "==1.33" 639 | }, 640 | "jsonpickle": { 641 | "hashes": [ 642 | "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e", 643 | "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b" 644 | ], 645 | "markers": "python_version >= '3.7'", 646 | "version": "==3.0.4" 647 | }, 648 | "jsonpointer": { 649 | "hashes": [ 650 | "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a", 651 | "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88" 652 | ], 653 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 654 | "version": "==2.4" 655 | }, 656 | "jsonschema": { 657 | "hashes": [ 658 | "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7", 659 | "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802" 660 | ], 661 | "markers": "python_version >= '3.8'", 662 | "version": "==4.22.0" 663 | }, 664 | "jsonschema-specifications": { 665 | "hashes": [ 666 | "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", 667 | "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" 668 | ], 669 | "markers": "python_version >= '3.8'", 670 | "version": "==2023.12.1" 671 | }, 672 | "junit-xml": { 673 | "hashes": [ 674 | "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f", 675 | "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732" 676 | ], 677 | "version": "==1.9" 678 | }, 679 | "mccabe": { 680 | "hashes": [ 681 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 682 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 683 | ], 684 | "markers": "python_version >= '3.6'", 685 | "version": "==0.7.0" 686 | }, 687 | "mpmath": { 688 | "hashes": [ 689 | "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", 690 | "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" 691 | ], 692 | "version": "==1.3.0" 693 | }, 694 | "networkx": { 695 | "hashes": [ 696 | "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36", 697 | "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61" 698 | ], 699 | "markers": "python_version >= '3.8'", 700 | "version": "==3.1" 701 | }, 702 | "packaging": { 703 | "hashes": [ 704 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 705 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 706 | ], 707 | "markers": "python_version >= '3.7'", 708 | "version": "==24.0" 709 | }, 710 | "pbr": { 711 | "hashes": [ 712 | "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", 713 | "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" 714 | ], 715 | "markers": "python_version >= '2.6'", 716 | "version": "==6.0.0" 717 | }, 718 | "pkgutil-resolve-name": { 719 | "hashes": [ 720 | "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", 721 | "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" 722 | ], 723 | "markers": "python_version < '3.9'", 724 | "version": "==1.3.10" 725 | }, 726 | "pluggy": { 727 | "hashes": [ 728 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 729 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 730 | ], 731 | "markers": "python_version >= '3.8'", 732 | "version": "==1.5.0" 733 | }, 734 | "pyasn1": { 735 | "hashes": [ 736 | "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", 737 | "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" 738 | ], 739 | "markers": "python_version >= '3.8'", 740 | "version": "==0.6.0" 741 | }, 742 | "pycodestyle": { 743 | "hashes": [ 744 | "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", 745 | "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" 746 | ], 747 | "markers": "python_version >= '3.8'", 748 | "version": "==2.11.1" 749 | }, 750 | "pydantic": { 751 | "hashes": [ 752 | "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5", 753 | "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc" 754 | ], 755 | "markers": "python_version >= '3.8'", 756 | "version": "==2.7.1" 757 | }, 758 | "pydantic-core": { 759 | "hashes": [ 760 | "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b", 761 | "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a", 762 | "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90", 763 | "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d", 764 | "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e", 765 | "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d", 766 | "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027", 767 | "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804", 768 | "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347", 769 | "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400", 770 | "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3", 771 | "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399", 772 | "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349", 773 | "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd", 774 | "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c", 775 | "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e", 776 | "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413", 777 | "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3", 778 | "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e", 779 | "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3", 780 | "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91", 781 | "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce", 782 | "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c", 783 | "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb", 784 | "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664", 785 | "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6", 786 | "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd", 787 | "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3", 788 | "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af", 789 | "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043", 790 | "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350", 791 | "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7", 792 | "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0", 793 | "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563", 794 | "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761", 795 | "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72", 796 | "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3", 797 | "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb", 798 | "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788", 799 | "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b", 800 | "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c", 801 | "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038", 802 | "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250", 803 | "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec", 804 | "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c", 805 | "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74", 806 | "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81", 807 | "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439", 808 | "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75", 809 | "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0", 810 | "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8", 811 | "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150", 812 | "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438", 813 | "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae", 814 | "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857", 815 | "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038", 816 | "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374", 817 | "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f", 818 | "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241", 819 | "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592", 820 | "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4", 821 | "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d", 822 | "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b", 823 | "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b", 824 | "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182", 825 | "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e", 826 | "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641", 827 | "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70", 828 | "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9", 829 | "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a", 830 | "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543", 831 | "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b", 832 | "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f", 833 | "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38", 834 | "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845", 835 | "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2", 836 | "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0", 837 | "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4", 838 | "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242" 839 | ], 840 | "markers": "python_version >= '3.8'", 841 | "version": "==2.18.2" 842 | }, 843 | "pydocstyle": { 844 | "hashes": [ 845 | "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", 846 | "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" 847 | ], 848 | "index": "pypi", 849 | "markers": "python_version >= '3.6'", 850 | "version": "==6.3.0" 851 | }, 852 | "pyflakes": { 853 | "hashes": [ 854 | "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", 855 | "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" 856 | ], 857 | "markers": "python_version >= '3.8'", 858 | "version": "==3.2.0" 859 | }, 860 | "pytest": { 861 | "hashes": [ 862 | "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", 863 | "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f" 864 | ], 865 | "index": "pypi", 866 | "markers": "python_version >= '3.8'", 867 | "version": "==8.2.0" 868 | }, 869 | "pytest-cov": { 870 | "hashes": [ 871 | "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", 872 | "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" 873 | ], 874 | "index": "pypi", 875 | "markers": "python_version >= '3.8'", 876 | "version": "==5.0.0" 877 | }, 878 | "pytest-mock": { 879 | "hashes": [ 880 | "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", 881 | "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" 882 | ], 883 | "index": "pypi", 884 | "markers": "python_version >= '3.8'", 885 | "version": "==3.14.0" 886 | }, 887 | "python-dateutil": { 888 | "hashes": [ 889 | "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", 890 | "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" 891 | ], 892 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 893 | "version": "==2.9.0.post0" 894 | }, 895 | "pyyaml": { 896 | "hashes": [ 897 | "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", 898 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 899 | "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", 900 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 901 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 902 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 903 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 904 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 905 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 906 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 907 | "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", 908 | "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", 909 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 910 | "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", 911 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 912 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 913 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 914 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 915 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 916 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 917 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 918 | "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", 919 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 920 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 921 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 922 | "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", 923 | "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", 924 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 925 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 926 | "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", 927 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 928 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 929 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 930 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 931 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 932 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 933 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 934 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 935 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 936 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 937 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 938 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 939 | "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", 940 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 941 | "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", 942 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 943 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 944 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 945 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 946 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 947 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 948 | ], 949 | "markers": "python_version >= '3.6'", 950 | "version": "==6.0.1" 951 | }, 952 | "referencing": { 953 | "hashes": [ 954 | "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", 955 | "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" 956 | ], 957 | "markers": "python_version >= '3.8'", 958 | "version": "==0.35.1" 959 | }, 960 | "regex": { 961 | "hashes": [ 962 | "sha256:031219782d97550c2098d9a68ce9e9eaefe67d2d81d8ff84c8354f9c009e720c", 963 | "sha256:0709ba544cf50bd5cb843df4b8bb6701bae2b70a8e88da9add8386cbca5c1385", 964 | "sha256:0a9f89d7db5ef6bdf53e5cc8e6199a493d0f1374b3171796b464a74ebe8e508a", 965 | "sha256:0bc94873ba11e34837bffd7e5006703abeffc4514e2f482022f46ce05bd25e67", 966 | "sha256:0ce56a923f4c01d7568811bfdffe156268c0a7aae8a94c902b92fe34c4bde785", 967 | "sha256:0faecb6d5779753a6066a3c7a0471a8d29fe25d9981ca9e552d6d1b8f8b6a594", 968 | "sha256:1118ba9def608250250f4b3e3f48c62f4562ba16ca58ede491b6e7554bfa09ff", 969 | "sha256:12446827f43c7881decf2c126762e11425de5eb93b3b0d8b581344c16db7047a", 970 | "sha256:14905ed75c7a6edf423eb46c213ed3f4507c38115f1ed3c00f4ec9eafba50e58", 971 | "sha256:15e593386ec6331e0ab4ac0795b7593f02ab2f4b30a698beb89fbdc34f92386a", 972 | "sha256:160ba087232c5c6e2a1e7ad08bd3a3f49b58c815be0504d8c8aacfb064491cd8", 973 | "sha256:161a206c8f3511e2f5fafc9142a2cc25d7fe9a1ec5ad9b4ad2496a7c33e1c5d2", 974 | "sha256:169fd0acd7a259f58f417e492e93d0e15fc87592cd1e971c8c533ad5703b5830", 975 | "sha256:193b7c6834a06f722f0ce1ba685efe80881de7c3de31415513862f601097648c", 976 | "sha256:1a3903128f9e17a500618e80c68165c78c741ebb17dd1a0b44575f92c3c68b02", 977 | "sha256:1d5bd666466c8f00a06886ce1397ba8b12371c1f1c6d1bef11013e9e0a1464a8", 978 | "sha256:224a9269f133564109ce668213ef3cb32bc72ccf040b0b51c72a50e569e9dc9e", 979 | "sha256:236cace6c1903effd647ed46ce6dd5d76d54985fc36dafc5256032886736c85d", 980 | "sha256:249fbcee0a277c32a3ce36d8e36d50c27c968fdf969e0fbe342658d4e010fbc8", 981 | "sha256:29d839829209f3c53f004e1de8c3113efce6d98029f044fa5cfee666253ee7e6", 982 | "sha256:2c8982ee19ccecabbaeac1ba687bfef085a6352a8c64f821ce2f43e6d76a9298", 983 | "sha256:2f30a5ab8902f93930dc6f627c4dd5da2703333287081c85cace0fc6e21c25af", 984 | "sha256:304e7e2418146ae4d0ef0e9ffa28f881f7874b45b4994cc2279b21b6e7ae50c8", 985 | "sha256:32e5f3b8e32918bfbdd12eca62e49ab3031125c454b507127ad6ecbd86e62fca", 986 | "sha256:334b79ce9c08f26b4659a53f42892793948a613c46f1b583e985fd5a6bf1c149", 987 | "sha256:33d19f0cde6838c81acffff25c7708e4adc7dd02896c9ec25c3939b1500a1778", 988 | "sha256:3799e36d60a35162bb35b2246d8bb012192b7437dff807ef79c14e7352706306", 989 | "sha256:42be5de7cc8c1edac55db92d82b68dc8e683b204d6f5414c5a51997a323d7081", 990 | "sha256:44b3267cea873684af022822195298501568ed44d542f9a2d9bebc0212e99069", 991 | "sha256:458d68d34fb74b906709735c927c029e62f7d06437a98af1b5b6258025223210", 992 | "sha256:45cc13d398b6359a7708986386f72bd156ae781c3e83a68a6d4cee5af04b1ce9", 993 | "sha256:4e7eaf9df15423d07b6050fb91f86c66307171b95ea53e2d87a7993b6d02c7f7", 994 | "sha256:4fad420b14ae1970a1f322e8ae84a1d9d89375eb71e1b504060ab2d1bfe68f3c", 995 | "sha256:504b5116e2bd1821efd815941edff7535e93372a098e156bb9dffde30264e798", 996 | "sha256:50e7e96a527488334379e05755b210b7da4a60fc5d6481938c1fa053e0c92184", 997 | "sha256:51d27844763c273a122e08a3e86e7aefa54ee09fb672d96a645ece0454d8425e", 998 | "sha256:5253dcb0bfda7214523de58b002eb0090cb530d7c55993ce5f6d17faf953ece7", 999 | "sha256:534efd2653ebc4f26fc0e47234e53bf0cb4715bb61f98c64d2774a278b58c846", 1000 | "sha256:560278c9975694e1f0bc50da187abf2cdc1e4890739ea33df2bc4a85eeef143e", 1001 | "sha256:571452362d552de508c37191b6abbbb660028b8b418e2d68c20779e0bc8eaaa8", 1002 | "sha256:62b5f7910b639f3c1d122d408421317c351e213ca39c964ad4121f27916631c6", 1003 | "sha256:696639a73ca78a380acfaa0a1f6dd8220616a99074c05bba9ba8bb916914b224", 1004 | "sha256:6ccdeef4584450b6f0bddd5135354908dacad95425fcb629fe36d13e48b60f32", 1005 | "sha256:70364a097437dd0a90b31cd77f09f7387ad9ac60ef57590971f43b7fca3082a5", 1006 | "sha256:7117cb7d6ac7f2e985f3d18aa8a1728864097da1a677ffa69e970ca215baebf1", 1007 | "sha256:7467ad8b0eac0b28e52679e972b9b234b3de0ea5cee12eb50091d2b68145fe36", 1008 | "sha256:7d35d4cc9270944e95f9c88af757b0c9fc43f396917e143a5756608462c5223b", 1009 | "sha256:7dda3091838206969c2b286f9832dff41e2da545b99d1cfaea9ebd8584d02708", 1010 | "sha256:853cc36e756ff673bf984e9044ccc8fad60b95a748915dddeab9488aea974c73", 1011 | "sha256:8722f72068b3e1156a4b2e1afde6810f1fc67155a9fa30a4b9d5b4bc46f18fb0", 1012 | "sha256:8c6c71cf92b09e5faa72ea2c68aa1f61c9ce11cb66fdc5069d712f4392ddfd00", 1013 | "sha256:903350bf44d7e4116b4d5898b30b15755d61dcd3161e3413a49c7db76f0bee5a", 1014 | "sha256:91b53dea84415e8115506cc62e441a2b54537359c63d856d73cb1abe05af4c9a", 1015 | "sha256:951be1eae7b47660412dc4938777a975ebc41936d64e28081bf2e584b47ec246", 1016 | "sha256:972b49f2fe1047b9249c958ec4fa1bdd2cf8ce305dc19d27546d5a38e57732d8", 1017 | "sha256:9a8625849387b9d558d528e263ecc9c0fbde86cfa5c2f0eef43fff480ae24d71", 1018 | "sha256:9cdbb1998da94607d5eec02566b9586f0e70d6438abf1b690261aac0edda7ab6", 1019 | "sha256:9e6d4d6ae1827b2f8c7200aaf7501c37cf3f3896c86a6aaf2566448397c823dd", 1020 | "sha256:aab65121229c2ecdf4a31b793d99a6a0501225bd39b616e653c87b219ed34a49", 1021 | "sha256:ab98016541543692a37905871a5ffca59b16e08aacc3d7d10a27297b443f572d", 1022 | "sha256:ad45f3bccfcb00868f2871dce02a755529838d2b86163ab8a246115e80cfb7d6", 1023 | "sha256:b43b78f9386d3d932a6ce5af4b45f393d2e93693ee18dc4800d30a8909df700e", 1024 | "sha256:b66421f8878a0c82fc0c272a43e2121c8d4c67cb37429b764f0d5ad70b82993b", 1025 | "sha256:ba034c8db4b264ef1601eb33cd23d87c5013b8fb48b8161debe2e5d3bd9156b0", 1026 | "sha256:bbdc5db2c98ac2bf1971ffa1410c87ca7a15800415f788971e8ba8520fc0fda9", 1027 | "sha256:bc0db93ad039fc2fe32ccd3dd0e0e70c4f3d6e37ae83f0a487e1aba939bd2fbd", 1028 | "sha256:bf7c8ee4861d9ef5b1120abb75846828c811f932d63311596ad25fa168053e00", 1029 | "sha256:bf9596cba92ce7b1fd32c7b07c6e3212c7eed0edc271757e48bfcd2b54646452", 1030 | "sha256:c43395a3b7cc9862801a65c6994678484f186ce13c929abab44fb8a9e473a55a", 1031 | "sha256:c46a76a599fcbf95f98755275c5527304cc4f1bb69919434c1e15544d7052910", 1032 | "sha256:ca23b41355ba95929e9505ee04e55495726aa2282003ed9b012d86f857d3e49b", 1033 | "sha256:cd832bd9b6120d6074f39bdfbb3c80e416848b07ac72910f1c7f03131a6debc3", 1034 | "sha256:cfa6d61a76c77610ba9274c1a90a453062bdf6887858afbe214d18ad41cf6bde", 1035 | "sha256:d8a0f0ab5453e409586b11ebe91c672040bc804ca98d03a656825f7890cbdf88", 1036 | "sha256:e91b1976358e17197157b405cab408a5f4e33310cda211c49fc6da7cffd0b2f0", 1037 | "sha256:ea057306ab469130167014b662643cfaed84651c792948891d003cf0039223a5", 1038 | "sha256:eda3dd46df535da787ffb9036b5140f941ecb91701717df91c9daf64cabef953", 1039 | "sha256:f03b1dbd4d9596dd84955bb40f7d885204d6aac0d56a919bb1e0ff2fb7e1735a", 1040 | "sha256:fa9335674d7c819674467c7b46154196c51efbaf5f5715187fd366814ba3fa39" 1041 | ], 1042 | "markers": "python_version >= '3.8'", 1043 | "version": "==2024.5.10" 1044 | }, 1045 | "rpds-py": { 1046 | "hashes": [ 1047 | "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee", 1048 | "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc", 1049 | "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc", 1050 | "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944", 1051 | "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20", 1052 | "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7", 1053 | "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4", 1054 | "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6", 1055 | "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6", 1056 | "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93", 1057 | "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633", 1058 | "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0", 1059 | "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360", 1060 | "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8", 1061 | "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139", 1062 | "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7", 1063 | "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a", 1064 | "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9", 1065 | "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26", 1066 | "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724", 1067 | "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72", 1068 | "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b", 1069 | "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09", 1070 | "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100", 1071 | "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3", 1072 | "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261", 1073 | "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3", 1074 | "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9", 1075 | "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b", 1076 | "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3", 1077 | "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de", 1078 | "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d", 1079 | "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e", 1080 | "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8", 1081 | "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff", 1082 | "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5", 1083 | "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c", 1084 | "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e", 1085 | "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e", 1086 | "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4", 1087 | "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8", 1088 | "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922", 1089 | "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338", 1090 | "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d", 1091 | "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8", 1092 | "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2", 1093 | "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72", 1094 | "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80", 1095 | "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644", 1096 | "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae", 1097 | "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163", 1098 | "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104", 1099 | "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d", 1100 | "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60", 1101 | "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a", 1102 | "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d", 1103 | "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07", 1104 | "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49", 1105 | "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10", 1106 | "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f", 1107 | "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2", 1108 | "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8", 1109 | "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7", 1110 | "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88", 1111 | "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65", 1112 | "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0", 1113 | "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909", 1114 | "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8", 1115 | "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c", 1116 | "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184", 1117 | "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397", 1118 | "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a", 1119 | "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346", 1120 | "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590", 1121 | "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333", 1122 | "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb", 1123 | "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74", 1124 | "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e", 1125 | "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d", 1126 | "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa", 1127 | "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f", 1128 | "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53", 1129 | "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1", 1130 | "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac", 1131 | "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0", 1132 | "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd", 1133 | "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611", 1134 | "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f", 1135 | "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c", 1136 | "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5", 1137 | "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab", 1138 | "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc", 1139 | "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43", 1140 | "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da", 1141 | "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac", 1142 | "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843", 1143 | "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e", 1144 | "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89", 1145 | "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64" 1146 | ], 1147 | "markers": "python_version >= '3.8'", 1148 | "version": "==0.18.1" 1149 | }, 1150 | "rsa": { 1151 | "hashes": [ 1152 | "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", 1153 | "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" 1154 | ], 1155 | "markers": "python_version >= '3.5' and python_version < '4'", 1156 | "version": "==4.7.2" 1157 | }, 1158 | "s3transfer": { 1159 | "hashes": [ 1160 | "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", 1161 | "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" 1162 | ], 1163 | "markers": "python_version >= '3.8'", 1164 | "version": "==0.10.1" 1165 | }, 1166 | "sarif-om": { 1167 | "hashes": [ 1168 | "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911", 1169 | "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98" 1170 | ], 1171 | "markers": "python_version >= '2.7'", 1172 | "version": "==1.0.4" 1173 | }, 1174 | "six": { 1175 | "hashes": [ 1176 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 1177 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 1178 | ], 1179 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1180 | "version": "==1.16.0" 1181 | }, 1182 | "snowballstemmer": { 1183 | "hashes": [ 1184 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 1185 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 1186 | ], 1187 | "version": "==2.2.0" 1188 | }, 1189 | "sympy": { 1190 | "hashes": [ 1191 | "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5", 1192 | "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8" 1193 | ], 1194 | "markers": "python_version >= '3.8'", 1195 | "version": "==1.12" 1196 | }, 1197 | "tomli": { 1198 | "hashes": [ 1199 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 1200 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 1201 | ], 1202 | "markers": "python_version < '3.11'", 1203 | "version": "==2.0.1" 1204 | }, 1205 | "typing-extensions": { 1206 | "hashes": [ 1207 | "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", 1208 | "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" 1209 | ], 1210 | "markers": "python_version >= '3.8'", 1211 | "version": "==4.11.0" 1212 | }, 1213 | "urllib3": { 1214 | "hashes": [ 1215 | "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", 1216 | "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" 1217 | ], 1218 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 1219 | "version": "==1.26.18" 1220 | }, 1221 | "zipp": { 1222 | "hashes": [ 1223 | "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", 1224 | "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" 1225 | ], 1226 | "markers": "python_version < '3.10'", 1227 | "version": "==3.18.1" 1228 | } 1229 | } 1230 | } 1231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-codebuild-logs 2 | 3 | ![Build Status](https://codebuild.us-east-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoicDlvblNsMkl0Y1hLelczd2EwZVRaVjd2eSs0ejVRWHlJTGtPSng0RDdFOGpsa0Z1YU1nMFNMd3RZbDBBaVZaR1lVMkVRNEFBM2x1NzdsTy9WdFFqeWlrPSIsIml2UGFyYW1ldGVyU3BlYyI6Ik13ZGZtcUVNTWZadnpvaGYiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) 4 | 5 | This serverless app solves a common complaint when using AWS CodeBuild as a CI solution: PR contributors don't have access to the build logs if the CI build fails on their PR branch. The app creates publicly accessible links to PR build logs for a given AWS CodeBuild project and posts them as a comment on the corresponding GitHub PR. 6 | 7 | Here is an example GitHub PR comment: 8 | 9 | ![Screenshot](https://github.com/jlhood/github-codebuild-logs/raw/master/images/screenshot.png) 10 | 11 | ## App Architecture 12 | 13 | ![App Architecture](https://github.com/jlhood/github-codebuild-logs/raw/master/images/app-architecture.png) 14 | 15 | 1. Contributors create or update a PR. 16 | 1. Assuming AWS CodeBuild is already setup as the CI solution for this repo, the PR triggers a new CI build. 17 | 1. Once the CI build completes (success or failure), a CloudWatch Event triggers an AWS Lambda function. 18 | 1. If the event is for a PR build, the Lambda function 19 | 1. copies the build log to an S3 bucket. Note, the build log auto-expires after a configurable number of days (default: 30). 20 | 1. publishes a comment on the GitHub PR with a publicly accessible link to the logs. Note, the app uses the CodeBuild project's GitHub OAUTH token to post the comment. 21 | 1. The logs link goes to an API Gateway endpoint, which redirects to a pre-signed URL for the build logs in the S3 bucket. 22 | 23 | ## Installation Instructions 24 | 25 | To attach this app to an existing AWS CodeBuild project in your AWS account, 26 | 27 | 1. Go to the app's page on the [Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:277187709615:applications~github-codebuild-logs) and click "Deploy" 28 | 1. Provide the CodeBuild project name and any other parameters (see parameter details below) and click "Deploy" 29 | 30 | Alternatively, if your CodeBuild project is defined in an AWS SAM template, this app can be embedded as a nested app inside that SAM template. To do this, visit the [app's page on the AWS Lambda Console](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-1:277187709615:applications/github-codebuild-logs). Click the "Copy as SAM Resource" button and paste the copied YAML into your SAM template. 31 | 32 | If you are an AWS CDK user, you can use the [aws-serverless.CfnApplication](https://awslabs.github.io/aws-cdk/refs/_aws-cdk_aws-serverless.html#cfnapplication) construct to embed this app in your CDK application. Here is a TypeScript example: 33 | 34 | ```typescript 35 | import serverless = require('@aws-cdk/aws-sam'); 36 | 37 | new serverless.CfnApplication(this, 'GitHubCodeBuildLogsSAR', { 38 | location: { 39 | applicationId: 'arn:aws:serverlessrepo:us-east-1:277187709615:applications/github-codebuild-logs', 40 | semanticVersion: '1.6.0' 41 | }, 42 | parameters: { 43 | CodeBuildProjectName: project.projectName 44 | } 45 | }); 46 | ``` 47 | 48 | ## App Parameters 49 | 50 | 1. `CodeBuildProjectName` (required) - Name of CodeBuild project this app is posting logs for. 51 | 1. `ExpirationInDays` (optional) - Number of days before a build's log page expires. Default: 30 52 | 1. `CodeBuildProjectCustomLogGroupName` (optional) - If the CodeBuild Project has a custom log group name, you can specify it here. If not provided, the app will assume the CodeBuild default log group name format of `/aws/codebuild/`. 53 | 1. `GitHubOAuthToken` (optional) - OAuth token used for writing comments to GitHub PRs. If not provided, the app will attempt to pull an OAuth token from the CodeBuild project. Note, if your CodeBuild project does not have a GitHub OAuth token, e.g., it is being used to build a public GitHub repo, then this parameter will be required for the app to function properly. 54 | * **NOTE:** The access token used requires `public_repo` permissions for public repositories 55 | or `repo` for private repositories. 56 | 1. `DeletePreviousComments` (optional) - Set to `true` to delete previously posted PR comments before posting a new one. Default: false 57 | 1. `LogLevel` (optional) - Log level for Lambda function logging, e.g., ERROR, INFO, DEBUG, etc. Default: INFO 58 | 1. `CommentOnSuccess` (optional) - Set to `false` to not publish a comment when build is successful. Default: true 59 | 1. `BuildEventTimeout` (optional) - Timeout for Process Build Event Lambda. Default: 60 60 | 61 | ## App Outputs 62 | 63 | 1. `ProcessBuildEventsFunctionName` - ProcessBuildEvents Lambda function name. 64 | 1. `ProcessBuildEventsFunctionArn` - ProcessBuildEvents Lambda function ARN. 65 | 1. `BuildLogsBucketName` - Build logs S3 bucket name. 66 | 1. `BuildLogsBucketArn` - Build logs S3 bucket ARN. 67 | 68 | ## Security Considerations 69 | 70 | The following precautions are taken when the `GitHubOAuthToken` parameter is provided since it's sensitive data: 71 | 72 | 1. The NoEcho option is used on the parameter so the value will never be shown by CloudFormation. 73 | 1. The app stores the value in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). 74 | 75 | ## License Summary 76 | 77 | This code is made available under the MIT license. See the LICENSE file. 78 | -------------------------------------------------------------------------------- /bin/requirements.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def convert_pipfile_lock_to_requirements(pipfile_lock_path): 4 | with open(pipfile_lock_path, 'r') as f: 5 | pipfile_lock = json.load(f) 6 | 7 | requirements = [] 8 | for package in pipfile_lock['default']: 9 | package_name = package 10 | package_details = pipfile_lock['default'][package] 11 | version = package_details['version'] 12 | markers = package_details.get('markers', '') 13 | if markers: 14 | markers = '; ' + markers 15 | requirement_line = f"{package_name}{version}{markers}" 16 | requirements.append(requirement_line) 17 | 18 | requirements_txt = '\n'.join(requirements) + '\n' # Add a newline at the end 19 | return requirements_txt 20 | 21 | if __name__ == '__main__': 22 | pipfile_lock_path = 'Pipfile.lock' 23 | requirements_txt = convert_pipfile_lock_to_requirements(pipfile_lock_path) 24 | print(requirements_txt) 25 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | commands: 5 | - pip install aws-sam-cli 6 | - make init 7 | build: 8 | commands: 9 | - make 10 | - make package 11 | artifacts: 12 | files: 13 | - dist/packaged-template.yml 14 | discard-paths: yes 15 | -------------------------------------------------------------------------------- /images/app-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlhood/github-codebuild-logs/07cbec1893a6b16d694b582c1a99bd5d19faeb68/images/app-architecture.png -------------------------------------------------------------------------------- /images/app-architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlhood/github-codebuild-logs/07cbec1893a6b16d694b582c1a99bd5d19faeb68/images/app-architecture.pptx -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlhood/github-codebuild-logs/07cbec1893a6b16d694b582c1a99bd5d19faeb68/images/screenshot.png -------------------------------------------------------------------------------- /src/build.py: -------------------------------------------------------------------------------- 1 | """Build log processing.""" 2 | 3 | import re 4 | import boto3 5 | from botocore.client import Config 6 | from urllib.parse import quote_plus 7 | 8 | import config 9 | import lambdalogging 10 | 11 | LOG = lambdalogging.getLogger(__name__) 12 | 13 | CODEBUILD = boto3.client('codebuild') 14 | CW_LOGS = boto3.client('logs') 15 | BUCKET = boto3.resource('s3', config=Config(signature_version='s3v4')).Bucket(config.BUCKET_NAME) 16 | 17 | 18 | class Build: 19 | """Encapsulate logic around CodeBuild builds and copying logs.""" 20 | 21 | def __init__(self, build_event): 22 | """Create new Build helper object.""" 23 | self._build_event = build_event 24 | self.id = build_event['detail']['build-id'] 25 | self.project_name = build_event['detail']['project-name'] 26 | self.status = build_event['detail']['build-status'] 27 | 28 | def get_pr_id(self): 29 | """If this build was for a PR branch, returns the PR ID, otherwise returns None.""" 30 | source_version = self._get_build_details().get('sourceVersion', "") 31 | matches = re.match(r'^pr\/(\d+)', source_version) 32 | if not matches: 33 | # check for refs pattern 34 | pattern = r'refs/pull/(\d+)/head' 35 | matches = re.search(pattern, source_version) 36 | if not matches: 37 | return None 38 | return int(matches.group(1)) 39 | 40 | @property 41 | def commit_id(self): 42 | """Return the commit ID for this build.""" 43 | return self._get_build_details()["resolvedSourceVersion"] 44 | 45 | def is_pr_build(self): 46 | """Return True if this build is associated with a PR.""" 47 | return self.get_pr_id() is not None 48 | 49 | def copy_logs(self): 50 | """Copy build logs to app S3 bucket and return a URL.""" 51 | log_info = self._get_build_details()['logs'] 52 | log_group = log_info['groupName'] 53 | log_stream = log_info['streamName'] 54 | paginator = CW_LOGS.get_paginator('filter_log_events') 55 | 56 | iter = paginator.paginate( 57 | logGroupName=log_group, 58 | logStreamNames=[log_stream] 59 | ) 60 | logs_content = ''.join([event['message'] for page in iter for event in page['events']]) 61 | 62 | BUCKET.put_object( 63 | Key=self._get_logs_key(), 64 | Body=logs_content, 65 | ContentType='text/plain' 66 | ) 67 | 68 | def get_logs_url(self): 69 | """Return URL to build logs.""" 70 | return '{}?key={}'.format(config.BUILD_LOGS_API_ENDPOINT, quote_plus(self._get_logs_key())) 71 | 72 | def _get_logs_key(self): 73 | log_stream = self._get_build_details()['logs']['streamName'] 74 | return '{}/build.log'.format(log_stream) 75 | 76 | def _get_build_details(self): 77 | if not hasattr(self, '_build_details'): 78 | response = CODEBUILD.batch_get_builds(ids=[self.id]) 79 | self._build_details = response['builds'][0] 80 | LOG.debug('Build %s details: %s', self.id, self._build_details) 81 | return self._build_details 82 | -------------------------------------------------------------------------------- /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 | BUCKET_NAME = os.getenv('BUILD_LOGS_BUCKET_NAME') 7 | PROJECT_NAME = os.getenv('CODEBUILD_PROJECT_NAME') 8 | EXPIRATION_IN_DAYS = int(os.getenv('EXPIRATION_IN_DAYS')) 9 | BUILD_LOGS_API_ENDPOINT = os.getenv('BUILD_LOGS_API_ENDPOINT') 10 | GITHUB_OAUTH_TOKEN_SECRET_ARN = os.getenv('GITHUB_OAUTH_TOKEN_SECRET_ARN') 11 | REGION = os.getenv('AWS_DEFAULT_REGION') 12 | DELETE_PREVIOUS_COMMENTS = os.getenv('DELETE_PREVIOUS_COMMENTS') == 'true' 13 | COMMENT_ON_SUCCESS = os.getenv('COMMENT_ON_SUCCESS') == 'true' 14 | -------------------------------------------------------------------------------- /src/getbuildlogs.py: -------------------------------------------------------------------------------- 1 | """Lambda function handler for processing build events.""" 2 | 3 | # must be the first import in files with lambda function handlers 4 | import lambdainit # noqa: F401 5 | 6 | import json 7 | from urllib.parse import unquote_plus 8 | 9 | import lambdalogging 10 | import s3link 11 | 12 | LOG = lambdalogging.getLogger(__name__) 13 | 14 | 15 | def handler(api_event, context): 16 | """Handle GET /buildlogs request. 17 | 18 | Redirects to pre-signed URL to build logs. 19 | """ 20 | LOG.debug('Received event: %s', api_event) 21 | log_key = api_event.get('queryStringParameters', {}).get('key') 22 | 23 | if not log_key: 24 | return _bad_request('missing expected query parameter: key') 25 | 26 | redirect_link = s3link.get_presigned_url(unquote_plus(log_key)) 27 | LOG.debug('redirect_link: %s', redirect_link) 28 | 29 | if redirect_link: 30 | return _redirect(redirect_link) 31 | else: 32 | return _not_found() 33 | 34 | 35 | def _bad_request(message): 36 | return _response( 37 | status_code=400, 38 | body=json.dumps({'error': message}) 39 | ) 40 | 41 | 42 | def _not_found(): 43 | return _response(status_code=404) 44 | 45 | 46 | def _redirect(redirect_link): 47 | return _response( 48 | status_code=307, 49 | headers={ 50 | 'Location': redirect_link 51 | } 52 | ) 53 | 54 | 55 | def _response(status_code=200, headers={}, body=None): 56 | response = { 57 | 'statusCode': status_code, 58 | 'headers': headers, 59 | 'body': body 60 | } 61 | LOG.debug('response: %s', response) 62 | return response 63 | -------------------------------------------------------------------------------- /src/github_proxy.py: -------------------------------------------------------------------------------- 1 | """Proxy for interacting with Github.""" 2 | 3 | import re 4 | 5 | import boto3 6 | from github import Github, GithubException 7 | 8 | import config 9 | import lambdalogging 10 | 11 | LOG = lambdalogging.getLogger(__name__) 12 | 13 | SAR_APP_URL = ('https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:277187709615:' 14 | 'applications~github-codebuild-logs') 15 | SAR_HOMEPAGE = 'https://aws.amazon.com/serverless/serverlessrepo/' 16 | 17 | HIDDEN_COMMENT = """ 18 | 21 | """ 22 | 23 | PR_COMMENT_TEMPLATE = f""" 24 | ### AWS CodeBuild CI Report 25 | 26 | * CodeBuild project: {{project_name}} 27 | * Commit ID: {{commit_id}} 28 | * Result: {{build_status}} 29 | * [Build Logs]({{logs_url}}) (available for {config.EXPIRATION_IN_DAYS} days) 30 | 31 | *Powered by [github-codebuild-logs]({SAR_APP_URL}),\ 32 | available on the [AWS Serverless Application Repository]({SAR_HOMEPAGE})* 33 | 34 | {HIDDEN_COMMENT} 35 | """ 36 | 37 | CODEBUILD = boto3.client('codebuild') 38 | SECRETS_MANAGER = boto3.client('secretsmanager') 39 | 40 | 41 | class GithubProxy: 42 | """Encapsulate interactions with Github.""" 43 | 44 | def __init__(self): 45 | """Initialize proxy.""" 46 | pass 47 | 48 | def publish_pr_comment(self, build): 49 | """Publish PR comment with link to build logs.""" 50 | pr_comment = PR_COMMENT_TEMPLATE.format( 51 | project_name=config.PROJECT_NAME, 52 | commit_id=build.commit_id, 53 | build_status=build.status, 54 | logs_url=build.get_logs_url(), 55 | ) 56 | 57 | repo = self._get_repo() 58 | LOG.debug('Publishing PR Comment: repo=%s/%s, pr_id=%s, comment=%s', 59 | self._github_owner, self._github_repo, build.get_pr_id(), pr_comment) 60 | repo.get_pull(build.get_pr_id()).create_issue_comment(pr_comment) 61 | 62 | def delete_previous_comments(self, build): 63 | """Delete previous PR comments.""" 64 | repo = self._get_repo() 65 | for comment in repo.get_issue(build.get_pr_id()).get_comments(): 66 | if HIDDEN_COMMENT in comment.body: # Check for hidden comment in body 67 | try: # Not critical, catch all GitHub exceptions here 68 | LOG.debug('Deleting previous comment: repo=%s/%s, pr_id=%s, comment_id=%s', 69 | self._github_owner, self._github_repo, build.get_pr_id(), comment.id) 70 | comment.delete() 71 | except GithubException as e: 72 | LOG.warning('Failed to delete previous comment: repo=%s/%s, pr_id=%s, comment_id=%s, error=%s', 73 | self._github_owner, self._github_repo, build.get_pr_id(), comment.id, str(e)) 74 | 75 | def _get_repo(self): 76 | if not hasattr(self, '_repo'): 77 | gh_client = self._get_client() 78 | self._repo = gh_client.get_user(self._github_owner).get_repo(self._github_repo) 79 | return self._repo 80 | 81 | def _get_client(self): 82 | if not hasattr(self, '_client'): 83 | self._init_client() 84 | return self._client 85 | 86 | def _init_client(self): 87 | self._init_github_info() 88 | self._client = Github(self._github_token) 89 | 90 | def _init_github_info(self): 91 | response = CODEBUILD.batch_get_projects( 92 | names=[config.PROJECT_NAME] 93 | ) 94 | 95 | project_details = response['projects'][0] 96 | if project_details['source']['type'] != 'GITHUB': 97 | raise RuntimeError( 98 | 'AWS CodeBuild project {} source is not GITHUB. Project source must be of type GITHUB'.format( 99 | config.PROJECT_NAME)) 100 | 101 | # if user provided an OAuth token to use, fetch it from secrets manager 102 | if config.GITHUB_OAUTH_TOKEN_SECRET_ARN: 103 | secret_response = SECRETS_MANAGER.get_secret_value(SecretId=config.GITHUB_OAUTH_TOKEN_SECRET_ARN) 104 | self._github_token = secret_response['SecretString'] 105 | # if user did not provide an OAuth token to use, try to get one from the CodeBuild project 106 | elif project_details['source'].get('auth', {}).get('type') == 'OAUTH': 107 | self._github_token = project_details['source']['auth']['resource'] 108 | else: 109 | raise RuntimeError( 110 | 'Could not get GitHub OAuth token from AWS CodeBuild project {}. Please use the GitHubOAuthToken app' 111 | ' parameter to specify a token to use when writing to GitHub.'.format(config.PROJECT_NAME)) 112 | 113 | github_location = project_details['source']['location'] 114 | matches = re.search(r'github\.com\/(.+)\/(.+)\.git$', github_location) 115 | if not matches: 116 | raise RuntimeError( 117 | 'Could not parse GitHub owner/repo name from AWS CodeBuild project {}. location={}'.format( 118 | config.PROJECT_NAME, github_location)) 119 | 120 | self._github_owner = matches.group(1) 121 | self._github_repo = matches.group(2) 122 | -------------------------------------------------------------------------------- /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/processbuildevents.py: -------------------------------------------------------------------------------- 1 | """Lambda function handler for processing build events.""" 2 | 3 | # must be the first import in files with lambda function handlers 4 | import lambdainit # noqa: F401 5 | 6 | from build import Build 7 | import config 8 | from github_proxy import GithubProxy 9 | import lambdalogging 10 | 11 | LOG = lambdalogging.getLogger(__name__) 12 | 13 | GITHUB = GithubProxy() 14 | 15 | 16 | def handler(build_event, context): 17 | """Process build events. 18 | 19 | If the build event is for the CodeBuild project this app is managing and it's specifically triggered by a PR, copy 20 | copy the build logs to the app S3 bucket and post a link to the logs as a comment on the GitHub PR. 21 | """ 22 | LOG.debug('Received event: %s', build_event) 23 | 24 | build = Build(build_event) 25 | 26 | if not build.is_pr_build(): 27 | LOG.debug('Not a PR build') 28 | return 29 | 30 | LOG.info('Copying build logs for PR build: project=%s, pr_id=%s, build_logs_url=%s', 31 | build.project_name, build.get_pr_id(), build.get_logs_url()) 32 | build.copy_logs() 33 | 34 | if config.DELETE_PREVIOUS_COMMENTS: 35 | GITHUB.delete_previous_comments(build) 36 | 37 | if build.status == 'SUCCEEDED' and not config.COMMENT_ON_SUCCESS: 38 | LOG.debug('Not publishing comment because build SUCCEEDED but COMMENT_ON_SUCCESS is set to false.') 39 | else: 40 | GITHUB.publish_pr_comment(build) 41 | -------------------------------------------------------------------------------- /src/s3link.py: -------------------------------------------------------------------------------- 1 | """Generate S3 link for an object.""" 2 | 3 | import boto3 4 | import botocore 5 | from botocore.client import Config 6 | 7 | import config 8 | import lambdalogging 9 | 10 | LOG = lambdalogging.getLogger(__name__) 11 | S3 = boto3.client('s3', config=Config(signature_version='s3v4')) 12 | BUCKET = boto3.resource('s3', config=Config(signature_version='s3v4')).Bucket(config.BUCKET_NAME) 13 | 14 | 15 | def get_presigned_url(key): 16 | """Generate presigned URL for given object key if it exists.""" 17 | if not key: 18 | return None 19 | 20 | try: 21 | BUCKET.Object(key).load() 22 | except botocore.exceptions.ClientError as e: 23 | if e.response['Error']['Code'] == '404': 24 | return None 25 | raise 26 | 27 | return S3.generate_presigned_url( 28 | ClientMethod='get_object', 29 | ExpiresIn=600, # 10 minutes 30 | Params={ 31 | 'Bucket': config.BUCKET_NAME, 32 | 'Key': key 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | This serverless app creates publicly accessible links to PR build logs for a given AWS CodeBuild project and posts them as a comment 5 | on the corresponding GitHub PR. 6 | 7 | Metadata: 8 | AWS::ServerlessRepo::Application: 9 | Name: github-codebuild-logs 10 | Description: > 11 | This serverless app creates publicly accessible links to PR build logs for a given AWS CodeBuild project and posts them as a comment 12 | on the corresponding GitHub PR. 13 | Author: James Hood 14 | # SPDX License Id, e.g., MIT, MIT-0, Apache-2.0. See https://spdx.org/licenses for more details 15 | SpdxLicenseId: MIT 16 | # paths are relative to .aws-sam/build directory 17 | LicenseUrl: LICENSE 18 | ReadmeUrl: README.md 19 | Labels: [github, pr, ci, codebuild, logs] 20 | HomePageUrl: https://github.com/jlhood/github-codebuild-logs 21 | # Update the semantic version and run sam publish to publish a new version of your app 22 | SemanticVersion: 1.6.0 23 | # best practice is to use git tags for each release and link to the version tag as your source code URL 24 | SourceCodeUrl: https://github.com/jlhood/github-codebuild-logs/tree/1.6.0 25 | 26 | Parameters: 27 | CodeBuildProjectName: 28 | Type: String 29 | Description: Name of CodeBuild project this app is posting logs for. 30 | ExpirationInDays: 31 | Type: Number 32 | Description: Number of days before a build's log page expires. 33 | MinValue: 1 34 | Default: 30 35 | CodeBuildProjectCustomLogGroupName: 36 | Type: String 37 | Default: "" 38 | Description: If the CodeBuild Project has a custom log group name, you can specify it here. If not provided, the app will assume the CodeBuild default log group name format of /aws/codebuild/. 39 | GitHubOAuthToken: 40 | Type: String 41 | Default: "" 42 | Description: OAuth token used for writing comments to GitHub PRs. If not provided, the app will attempt to pull an OAuth token from the CodeBuild project. 43 | NoEcho: true 44 | LogLevel: 45 | Type: String 46 | Description: Log level for Lambda function logging, e.g., ERROR, INFO, DEBUG, etc 47 | Default: INFO 48 | DeletePreviousComments: 49 | Type: String 50 | AllowedValues: 51 | - "true" 52 | - "false" 53 | Default: "false" 54 | Description: Set to "true" to delete previously posted PR comments before posting a new one. 55 | CommentOnSuccess: 56 | Type: String 57 | AllowedValues: 58 | - "true" 59 | - "false" 60 | Default: "true" 61 | Description: Set to "false" to not publish a comment when build is successful. 62 | BuildEventTimeout: 63 | Type: Number 64 | MinValue: 1 65 | MaxValue: 900 66 | Default: 60 67 | Description: Timeout for Process Build Event Lambda. 68 | 69 | Conditions: 70 | UseDefaultProjectLogGroupName: 71 | !Equals [!Ref CodeBuildProjectCustomLogGroupName, ''] 72 | GitHubOAuthTokenProvided: 73 | !Not [!Equals [!Ref GitHubOAuthToken, '']] 74 | 75 | Resources: 76 | ProcessBuildEvents: 77 | Type: AWS::Serverless::Function 78 | Properties: 79 | CodeUri: src/ 80 | Handler: processbuildevents.handler 81 | Runtime: python3.12 82 | Tracing: Active 83 | Timeout: !Ref BuildEventTimeout 84 | Policies: 85 | - S3CrudPolicy: 86 | BucketName: !Ref BuildLogs 87 | - Statement: 88 | Effect: Allow 89 | Action: 90 | - codebuild:BatchGetBuilds 91 | - codebuild:BatchGetProjects 92 | Resource: !Sub arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProjectName} 93 | - Statement: 94 | Effect: Allow 95 | Action: 96 | - logs:FilterLogEvents 97 | Resource: !Sub 98 | - arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${logGroupName}:log-stream:* 99 | - logGroupName: !If 100 | - UseDefaultProjectLogGroupName 101 | - !Sub /aws/codebuild/${CodeBuildProjectName} 102 | - !Ref CodeBuildProjectCustomLogGroupName 103 | - !If 104 | - GitHubOAuthTokenProvided 105 | - AWSSecretsManagerGetSecretValuePolicy: 106 | SecretArn: !Ref GitHubOAuthTokenSecret 107 | - !Ref AWS::NoValue 108 | Environment: 109 | Variables: 110 | LOG_LEVEL: !Ref LogLevel 111 | BUILD_LOGS_BUCKET_NAME: !Ref BuildLogs 112 | CODEBUILD_PROJECT_NAME: !Ref CodeBuildProjectName 113 | EXPIRATION_IN_DAYS: !Ref ExpirationInDays 114 | BUILD_LOGS_API_ENDPOINT: !Sub "https://${BuildLogsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/buildlogs" 115 | GITHUB_OAUTH_TOKEN_SECRET_ARN: !If 116 | - GitHubOAuthTokenProvided 117 | - !Ref GitHubOAuthTokenSecret 118 | - '' 119 | DELETE_PREVIOUS_COMMENTS: !Ref DeletePreviousComments 120 | COMMENT_ON_SUCCESS: !Ref CommentOnSuccess 121 | Events: 122 | BuildStatus: 123 | Type: CloudWatchEvent 124 | Properties: 125 | Pattern: 126 | source: 127 | - aws.codebuild 128 | 'detail-type': 129 | - CodeBuild Build State Change 130 | detail: 131 | 'project-name': 132 | - !Ref CodeBuildProjectName 133 | 'build-status': 134 | - SUCCEEDED 135 | - FAILED 136 | 137 | GitHubOAuthTokenSecret: 138 | Condition: GitHubOAuthTokenProvided 139 | Type: AWS::SecretsManager::Secret 140 | DeletionPolicy: Delete 141 | UpdateReplacePolicy: Retain 142 | Properties: 143 | Name: !Sub /github-codebuild-logs/${AWS::StackName}/GitHubOAuthToken 144 | SecretString: !Ref GitHubOAuthToken 145 | 146 | GetBuildLogs: 147 | Type: AWS::Serverless::Function 148 | Properties: 149 | CodeUri: src/ 150 | Handler: getbuildlogs.handler 151 | Runtime: python3.12 152 | Tracing: Active 153 | Timeout: 20 154 | Policies: 155 | - S3ReadPolicy: 156 | BucketName: !Ref BuildLogs 157 | Environment: 158 | Variables: 159 | LOG_LEVEL: !Ref LogLevel 160 | BUILD_LOGS_BUCKET_NAME: !Ref BuildLogs 161 | CODEBUILD_PROJECT_NAME: !Ref CodeBuildProjectName 162 | EXPIRATION_IN_DAYS: !Ref ExpirationInDays 163 | Events: 164 | Api: 165 | Type: Api 166 | Properties: 167 | RestApiId: !Ref BuildLogsApi 168 | Path: /buildlogs 169 | Method: get 170 | 171 | BuildLogsApi: 172 | Type: AWS::Serverless::Api 173 | DeletionPolicy: Retain 174 | UpdateReplacePolicy: Retain 175 | Properties: 176 | StageName: Prod 177 | MethodSettings: 178 | # This is a publicly accessible endpoint so don't let someone rack up costs by calling our endpoint over and over again. 179 | - ThrottlingRateLimit: 1 180 | ThrottlingBurstLimit: 2 181 | ResourcePath: '/*' 182 | HttpMethod: '*' 183 | 184 | BuildLogs: 185 | Type: AWS::S3::Bucket 186 | DeletionPolicy: Retain 187 | UpdateReplacePolicy: Retain 188 | Properties: 189 | LifecycleConfiguration: 190 | Rules: 191 | - ExpirationInDays: !Ref ExpirationInDays 192 | Status: Enabled 193 | 194 | Outputs: 195 | ProcessBuildEventsFunctionName: 196 | Description: "ProcessBuildEvents Lambda Function Name" 197 | Value: !Ref ProcessBuildEvents 198 | ProcessBuildEventsFunctionArn: 199 | Description: "ProcessBuildEvents Lambda Function ARN" 200 | Value: !GetAtt ProcessBuildEvents.Arn 201 | BuildLogsBucketName: 202 | Description: "Build logs S3 bucket name" 203 | Value: !Ref BuildLogs 204 | BuildLogsBucketArn: 205 | Description: "Build logs S3 bucket ARN" 206 | Value: !GetAtt BuildLogs.Arn 207 | -------------------------------------------------------------------------------- /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['BUILD_LOGS_BUCKET_NAME'] = test_constants.BUCKET_NAME 14 | os.environ['CODEBUILD_PROJECT_NAME'] = test_constants.PROJECT_NAME 15 | os.environ['EXPIRATION_IN_DAYS'] = str(test_constants.EXPIRATION_IN_DAYS) 16 | os.environ['BUILD_LOGS_API_ENDPOINT'] = test_constants.BUILD_LOGS_API_ENDPOINT 17 | os.environ['GITHUB_OAUTH_TOKEN_SECRET_ARN'] = test_constants.GITHUB_OAUTH_TOKEN_SECRET_ARN 18 | os.environ['COMMENT_ON_SUCCESS'] = test_constants.COMMENT_ON_SUCCESS 19 | os.environ['AWS_DEFAULT_REGION'] = test_constants.REGION 20 | -------------------------------------------------------------------------------- /test/unit/test_build.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import build 4 | import test_constants 5 | 6 | BUILD_ID = 'some-build' 7 | BUILD_STATUS = 'SUCCEEDED' 8 | LOG_GROUP_NAME = "log-group" 9 | LOG_STREAM_NAME = "log-stream" 10 | 11 | 12 | @pytest.fixture 13 | def mock_codebuild(mocker): 14 | mocker.patch.object(build, 'CODEBUILD') 15 | return build.CODEBUILD 16 | 17 | 18 | @pytest.fixture 19 | def mock_cw_logs(mocker): 20 | mocker.patch.object(build, 'CW_LOGS') 21 | return build.CW_LOGS 22 | 23 | 24 | @pytest.fixture 25 | def mock_bucket(mocker): 26 | mocker.patch.object(build, 'BUCKET') 27 | return build.BUCKET 28 | 29 | 30 | def test_init(): 31 | build_obj = build.Build(_mock_build_event()) 32 | assert build_obj.id == BUILD_ID 33 | assert build_obj.project_name == test_constants.PROJECT_NAME 34 | assert build_obj.status == BUILD_STATUS 35 | 36 | 37 | def test_get_pr_id_not_pr(mocker, mock_codebuild): 38 | _mock_build_details('master') 39 | build_obj = build.Build(_mock_build_event()) 40 | assert build_obj.get_pr_id() is None 41 | 42 | 43 | def test_get_pr_id_no_source_version(mocker, mock_codebuild): 44 | _mock_build_details(None) 45 | build_obj = build.Build(_mock_build_event()) 46 | assert build_obj.get_pr_id() is None 47 | 48 | 49 | def test_get_pr_id_is_pr(mocker, mock_codebuild): 50 | _mock_build_details('pr/123') 51 | build_obj = build.Build(_mock_build_event()) 52 | assert build_obj.get_pr_id() == 123 53 | 54 | 55 | def test_get_pr_id_is_pr_refs_pattern(mocker, mock_codebuild): 56 | _mock_build_details('refs/pull/123/head^{2adff25fccedf7c08117b3fc93a7eca896c19060}') 57 | build_obj = build.Build(_mock_build_event()) 58 | assert build_obj.get_pr_id() == 123 59 | 60 | 61 | def test_is_pr_build_not_pr(mocker, mock_codebuild): 62 | _mock_build_details('master') 63 | build_obj = build.Build(_mock_build_event()) 64 | assert build_obj.is_pr_build() is False 65 | 66 | 67 | def test_is_pr_build_is_pr(mocker, mock_codebuild): 68 | _mock_build_details('pr/123') 69 | build_obj = build.Build(_mock_build_event()) 70 | assert build_obj.is_pr_build() is True 71 | 72 | 73 | def test_copy_logs(mocker, mock_codebuild, mock_cw_logs, mock_bucket): 74 | mock_cw_logs.get_paginator.return_value.paginate.return_value = [ 75 | { 76 | 'events': [ 77 | { 78 | 'message': 'foo', 79 | }, 80 | { 81 | 'message': 'bar', 82 | } 83 | ] 84 | }, 85 | { 86 | 'events': [ 87 | { 88 | 'message': 'baz', 89 | }, 90 | { 91 | 'message': 'blah', 92 | } 93 | ] 94 | }, 95 | ] 96 | _mock_build_details('pr/123') 97 | 98 | build_obj = build.Build(_mock_build_event()) 99 | build_obj.copy_logs() 100 | 101 | mock_codebuild.batch_get_builds.assert_called_once_with(ids=[BUILD_ID]) 102 | 103 | mock_cw_logs.get_paginator.assert_called_once_with('filter_log_events') 104 | mock_cw_logs.get_paginator.return_value.paginate.assert_called_once_with( 105 | logGroupName=LOG_GROUP_NAME, 106 | logStreamNames=[LOG_STREAM_NAME] 107 | ) 108 | 109 | mock_bucket.put_object.assert_called_once_with( 110 | Key=LOG_STREAM_NAME + '/build.log', 111 | Body='foobarbazblah', 112 | ContentType="text/plain" 113 | ) 114 | 115 | 116 | def test_get_logs_url(mocker, mock_codebuild): 117 | _mock_build_details('pr/123') 118 | build_obj = build.Build(_mock_build_event()) 119 | assert build_obj.get_logs_url() == test_constants.BUILD_LOGS_API_ENDPOINT + '?key=' + LOG_STREAM_NAME + '%2Fbuild.log' 120 | 121 | 122 | def _mock_build_event(): 123 | return { 124 | 'detail': { 125 | 'build-id': BUILD_ID, 126 | 'project-name': test_constants.PROJECT_NAME, 127 | 'build-status': BUILD_STATUS 128 | } 129 | } 130 | 131 | 132 | def _mock_build_details(source_version): 133 | build_details = { 134 | 'builds': [ 135 | { 136 | 'logs': { 137 | 'groupName': LOG_GROUP_NAME, 138 | 'streamName': LOG_STREAM_NAME, 139 | } 140 | } 141 | ] 142 | } 143 | if source_version: 144 | build_details['builds'][0]['sourceVersion'] = source_version 145 | 146 | build.CODEBUILD.batch_get_builds.return_value = build_details 147 | -------------------------------------------------------------------------------- /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 | BUCKET_NAME = "TestBucket" 7 | PROJECT_NAME = 'TestProject' 8 | EXPIRATION_IN_DAYS = 20 9 | BUILD_LOGS_API_ENDPOINT = 'https://foo.com/bar' 10 | GITHUB_OAUTH_TOKEN_SECRET_ARN = '' 11 | COMMENT_ON_SUCCESS = 'true' 12 | REGION = 'us-east-1' 13 | -------------------------------------------------------------------------------- /test/unit/test_getbuildlogs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | import getbuildlogs 5 | import test_constants 6 | 7 | 8 | @pytest.fixture 9 | def mock_s3link(mocker): 10 | mocker.patch.object(getbuildlogs, 's3link') 11 | return getbuildlogs.s3link 12 | 13 | 14 | def test_handler_no_log_key(mock_s3link): 15 | api_event = _mock_api_event() 16 | response = getbuildlogs.handler(api_event, None) 17 | 18 | assert response == { 19 | 'statusCode': 400, 20 | 'headers': {}, 21 | 'body': json.dumps({ 22 | 'error': 'missing expected query parameter: key' 23 | }) 24 | } 25 | 26 | mock_s3link.get_presigned_url.assert_not_called() 27 | 28 | 29 | def test_handler_log_key_no_exist(mock_s3link): 30 | mock_s3link.get_presigned_url.return_value = None 31 | 32 | api_event = _mock_api_event({'key': 'no_exist'}) 33 | response = getbuildlogs.handler(api_event, None) 34 | 35 | assert response == { 36 | 'statusCode': 404, 37 | 'headers': {}, 38 | 'body': None 39 | } 40 | 41 | mock_s3link.get_presigned_url.assert_called_with('no_exist') 42 | 43 | 44 | def test_handler_happycase(mock_s3link): 45 | mock_s3link.get_presigned_url.return_value = 'url' 46 | 47 | api_event = _mock_api_event({'key': 'foo%2Fbuild.log'}) 48 | response = getbuildlogs.handler(api_event, None) 49 | 50 | assert response == { 51 | 'statusCode': 307, 52 | 'headers': { 53 | 'Location': 'url' 54 | }, 55 | 'body': None 56 | } 57 | 58 | mock_s3link.get_presigned_url.assert_called_with('foo/build.log') 59 | 60 | 61 | def _mock_api_event(query_parameters={}): 62 | return { 63 | 'queryStringParameters': query_parameters 64 | } 65 | -------------------------------------------------------------------------------- /test/unit/test_github_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import MagicMock 3 | 4 | import github_proxy 5 | import test_constants 6 | 7 | GITHUB_OWNER = 'gh-user' 8 | GITHUB_REPO = 'gh-repo' 9 | GITHUB_LOCATION = 'https://github.com/{}/{}.git'.format(GITHUB_OWNER, GITHUB_REPO) 10 | CODEBUILD_GITHUB_TOKEN = 'shhh!!' 11 | SECRETS_MANAGER_GITHUB_TOKEN = "don't tell!!" 12 | BUILD_STATUS = 'SUCCEEDED' 13 | PR_ID = 5 14 | COMMIT_ID = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' 15 | LOGS_URL = 'https://foo.com' 16 | 17 | 18 | @pytest.fixture 19 | def mock_config(mocker): 20 | mocker.patch.object(github_proxy, 'config') 21 | github_proxy.config.configure_mock( 22 | PROJECT_NAME=test_constants.PROJECT_NAME, 23 | EXPIRATION_IN_DAYS=test_constants.EXPIRATION_IN_DAYS, 24 | GITHUB_OAUTH_TOKEN_SECRET_ARN='' 25 | ) 26 | return github_proxy.config 27 | 28 | 29 | @pytest.fixture 30 | def mock_codebuild(mocker): 31 | mocker.patch.object(github_proxy, 'CODEBUILD') 32 | github_proxy.CODEBUILD.batch_get_projects.return_value = { 33 | 'projects': [ 34 | { 35 | 'source': { 36 | 'type': 'GITHUB', 37 | 'location': GITHUB_LOCATION, 38 | 'auth': { 39 | 'type': 'OAUTH', 40 | 'resource': CODEBUILD_GITHUB_TOKEN 41 | } 42 | } 43 | } 44 | ] 45 | } 46 | return github_proxy.CODEBUILD 47 | 48 | 49 | @pytest.fixture 50 | def mock_secretsmanager(mocker): 51 | mocker.patch.object(github_proxy, 'SECRETS_MANAGER') 52 | github_proxy.SECRETS_MANAGER.get_secret_value.return_value = { 53 | 'SecretString': SECRETS_MANAGER_GITHUB_TOKEN 54 | } 55 | return github_proxy.SECRETS_MANAGER 56 | 57 | 58 | @pytest.fixture 59 | def mock_github(mocker): 60 | mocker.patch.object(github_proxy, 'Github') 61 | return github_proxy.Github.return_value 62 | 63 | @pytest.fixture 64 | def mock_log(mocker): 65 | mocker.patch.object(github_proxy, 'LOG') 66 | return github_proxy.LOG 67 | 68 | def test_publish_pr_comment(mocker, mock_config, mock_codebuild, mock_secretsmanager, mock_github): 69 | build = MagicMock(status=BUILD_STATUS) 70 | build.get_logs_url.return_value = LOGS_URL 71 | build.get_pr_id.return_value = PR_ID 72 | build.commit_id = COMMIT_ID 73 | 74 | proxy = github_proxy.GithubProxy() 75 | proxy.publish_pr_comment(build) 76 | 77 | mock_codebuild.batch_get_projects.assert_called_once_with( 78 | names=[test_constants.PROJECT_NAME] 79 | ) 80 | mock_secretsmanager.get_secret_value.assert_not_called() 81 | github_proxy.Github.assert_called_once_with(CODEBUILD_GITHUB_TOKEN) 82 | 83 | mock_github.get_user.assert_called_once_with(GITHUB_OWNER) 84 | mock_github.get_user.return_value.get_repo.assert_called_once_with(GITHUB_REPO) 85 | mock_repo = mock_github.get_user.return_value.get_repo.return_value 86 | mock_repo.get_pull.assert_called_once_with(PR_ID) 87 | mock_pr = mock_repo.get_pull.return_value 88 | 89 | expected_comment = github_proxy.PR_COMMENT_TEMPLATE.format( 90 | project_name=test_constants.PROJECT_NAME, 91 | commit_id=COMMIT_ID, 92 | build_status=BUILD_STATUS, 93 | logs_url=LOGS_URL, 94 | ) 95 | mock_pr.create_issue_comment.assert_called_once_with(expected_comment) 96 | 97 | def test_delete_previous_comments(mocker, mock_config, mock_codebuild, mock_secretsmanager, mock_github, mock_log): 98 | build = MagicMock(status=BUILD_STATUS) 99 | build.get_pr_id.return_value = PR_ID 100 | 101 | mock_repo = mock_github.get_user.return_value.get_repo.return_value 102 | mock_issue = mock_repo.get_issue.return_value 103 | 104 | comment1 = MagicMock() 105 | comment2 = MagicMock(body=github_proxy.HIDDEN_COMMENT) 106 | comment2.delete.side_effect = github_proxy.GithubException('status', 'data', {}) 107 | comment3 = MagicMock(body='with ' + github_proxy.HIDDEN_COMMENT + ' comment') 108 | mock_issue.get_comments.return_value = [comment1, comment2, comment3] 109 | 110 | proxy = github_proxy.GithubProxy() 111 | proxy.delete_previous_comments(build) 112 | 113 | mock_repo.get_issue.assert_called_once_with(PR_ID) 114 | mock_issue.get_comments.assert_called_once() 115 | comment1.delete.assert_not_called() 116 | comment2.delete.assert_called_once() 117 | comment3.delete.assert_called_once() 118 | mock_log.warning.assert_called_once() 119 | 120 | def test_init_github_info_auth_with_secrets_manager_arn(mocker, mock_config, mock_codebuild, mock_secretsmanager): 121 | secret_arn = 'arn:secret' 122 | mock_config.configure_mock(GITHUB_OAUTH_TOKEN_SECRET_ARN=secret_arn) 123 | 124 | proxy = github_proxy.GithubProxy() 125 | proxy._init_github_info() 126 | 127 | assert proxy._github_token == SECRETS_MANAGER_GITHUB_TOKEN 128 | mock_secretsmanager.get_secret_value.assert_called_once_with(SecretId=secret_arn) 129 | 130 | 131 | def test_init_github_info_type_not_github(mocker, mock_config, mock_codebuild, mock_secretsmanager): 132 | mock_codebuild.batch_get_projects.return_value['projects'][0]['source']['type'] = 'NOT_GITHUB' 133 | proxy = github_proxy.GithubProxy() 134 | with pytest.raises(RuntimeError): 135 | proxy._init_github_info() 136 | 137 | 138 | def test_init_github_info_auth_type_not_oauth(mocker, mock_config, mock_codebuild, mock_secretsmanager): 139 | mock_codebuild.batch_get_projects.return_value['projects'][0]['source']['auth']['type'] = 'NOT_OAUTH' 140 | proxy = github_proxy.GithubProxy() 141 | with pytest.raises(RuntimeError): 142 | proxy._init_github_info() 143 | 144 | 145 | def test_init_github_info_invalid_source_location(mocker, mock_config, mock_codebuild, mock_secretsmanager): 146 | mock_codebuild.batch_get_projects.return_value['projects'][0]['source']['location'] = 'bad-location' 147 | proxy = github_proxy.GithubProxy() 148 | with pytest.raises(RuntimeError): 149 | proxy._init_github_info() 150 | -------------------------------------------------------------------------------- /test/unit/test_processbuildevents.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import processbuildevents 4 | import test_constants 5 | 6 | 7 | @pytest.fixture 8 | def mock_build(mocker): 9 | mocker.patch.object(processbuildevents, 'Build') 10 | mock_build = processbuildevents.Build.return_value 11 | mock_build.project_name = test_constants.PROJECT_NAME 12 | mock_build.is_pr_build.return_value = True 13 | mock_build.status = 'SUCCEEDED' 14 | return mock_build 15 | 16 | 17 | @pytest.fixture 18 | def mock_github(mocker): 19 | mocker.patch.object(processbuildevents, 'GITHUB') 20 | return processbuildevents.GITHUB 21 | 22 | 23 | def test_handler(mocker, mock_build, mock_github): 24 | build_event = _mock_build_event() 25 | processbuildevents.handler(build_event, None) 26 | 27 | processbuildevents.Build.assert_called_once_with(build_event) 28 | mock_build.copy_logs.assert_called_once() 29 | mock_github.publish_pr_comment.assert_called_once_with(mock_build) 30 | mock_github.delete_previous_comments.assert_not_called() 31 | 32 | 33 | def test_handler_not_pr_build(mocker, mock_build, mock_github): 34 | mock_build.is_pr_build.return_value = False 35 | 36 | processbuildevents.handler(_mock_build_event(), None) 37 | 38 | mock_build.copy_logs.assert_not_called() 39 | mock_github.publish_pr_comment.assert_not_called() 40 | mock_github.delete_previous_comments.assert_not_called() 41 | 42 | 43 | def test_handler_delete_previous_commments(mocker, mock_build, mock_github): 44 | processbuildevents.config.DELETE_PREVIOUS_COMMENTS = True 45 | 46 | build_event = _mock_build_event() 47 | processbuildevents.handler(build_event, None) 48 | 49 | mock_github.delete_previous_comments.assert_called_once_with(mock_build) 50 | 51 | def test_handler_successful_pr_build_no_comment(mocker, mock_build, mock_github): 52 | processbuildevents.config.COMMENT_ON_SUCCESS = False 53 | 54 | build_event = _mock_build_event() 55 | processbuildevents.handler(build_event, None) 56 | 57 | mock_github.publish_pr_comment.assert_not_called() 58 | 59 | def test_handler_failure_pr_build_comment(mocker, mock_build, mock_github): 60 | processbuildevents.config.COMMENT_ON_SUCCESS = False 61 | mock_build.status = 'FAILED' 62 | 63 | build_event = _mock_build_event() 64 | processbuildevents.handler(build_event, None) 65 | 66 | mock_github.publish_pr_comment.assert_called_once_with(mock_build) 67 | 68 | 69 | def _mock_build_event(): 70 | return { 71 | 'detail': { 72 | 'build-id': 'some-build' 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/unit/test_s3link.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import botocore 4 | 5 | import s3link 6 | import test_constants 7 | 8 | 9 | @pytest.fixture 10 | def mock_s3(mocker): 11 | mocker.patch.object(s3link, 'S3') 12 | return s3link.S3 13 | 14 | 15 | @pytest.fixture 16 | def mock_bucket(mocker): 17 | mocker.patch.object(s3link, 'BUCKET') 18 | return s3link.BUCKET 19 | 20 | 21 | def test_get_presigned_url_none_key(mock_s3, mock_bucket): 22 | assert s3link.get_presigned_url(None) is None 23 | mock_bucket.Object.assert_not_called() 24 | mock_s3.generate_presigned_url.assert_not_called() 25 | 26 | 27 | def test_get_presigned_url_key_not_found(mock_s3, mock_bucket): 28 | mock_bucket.Object.return_value.load.side_effect = botocore.exceptions.ClientError( 29 | { 30 | 'Error': { 31 | 'Code': '404' 32 | } 33 | }, None 34 | ) 35 | 36 | assert s3link.get_presigned_url('foo') is None 37 | mock_bucket.Object.assert_called_with('foo') 38 | mock_s3.generate_presigned_url.assert_not_called() 39 | 40 | 41 | def test_get_presigned_url_other_exception(mock_s3, mock_bucket): 42 | mock_bucket.Object.return_value.load.side_effect = botocore.exceptions.ClientError( 43 | { 44 | 'Error': { 45 | 'Code': 'boom!' 46 | } 47 | }, None 48 | ) 49 | 50 | with pytest.raises(botocore.exceptions.ClientError): 51 | s3link.get_presigned_url('foo') 52 | 53 | mock_bucket.Object.assert_called_with('foo') 54 | mock_s3.generate_presigned_url.assert_not_called() 55 | 56 | 57 | def test_get_presigned_url_key_exists(mock_s3, mock_bucket): 58 | mock_s3.generate_presigned_url.return_value = 'presigned-url' 59 | 60 | assert s3link.get_presigned_url('foo') == 'presigned-url' 61 | 62 | mock_bucket.Object.assert_called_with('foo') 63 | mock_bucket.Object.return_value.load.assert_called_with() 64 | mock_s3.generate_presigned_url.assert_called_with( 65 | ClientMethod='get_object', 66 | ExpiresIn=600, 67 | Params={ 68 | 'Bucket': test_constants.BUCKET_NAME, 69 | 'Key': 'foo' 70 | } 71 | ) 72 | --------------------------------------------------------------------------------