├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pre-commit │ └── lint.sh └── workflows │ ├── misc_items │ └── example_event_release.json │ ├── release.yml │ ├── serverless_deploy.yml │ └── version_push.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── design.md ├── images │ ├── installed_package.png │ └── lambda_cache.png ├── index.md ├── install.md ├── reference.md └── user_guide.md ├── lambda_cache ├── __init__.py ├── caching_logic.py ├── exceptions.py ├── s3.py ├── secrets_manager.py └── ssm.py ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml └── tests ├── .coveragerc ├── __init__.py ├── acceptance_tests ├── _test_ssm.py └── serverless.yml ├── context_object.py ├── helper_functions.py ├── integration_tests ├── .gitignore ├── __init__.py ├── _test_s3.py ├── _test_secrets.py ├── _test_ssm.py ├── helper_functions.py ├── package.json ├── serverless.yml ├── test_deployed_lambda.py ├── test_multi_handler.py ├── test_multi_handler_2.py ├── test_s3.py └── test_secrets.py ├── test_env ├── s3.tf ├── test_data │ ├── s3_new.json │ └── s3_old.json └── versions.tf ├── test_generic.py ├── test_s3.py ├── test_secrets_manager_cache.py ├── test_ssm_cache.py ├── test_ssm_invalid.py ├── test_ssm_multi_param.py ├── test_ssm_param_assignment.py └── variables_data.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: " 5 | labels: '' 6 | assignees: keithrozario 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | What is the permission policy of your function? 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: keithrozario 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pre-commit/lint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | poetry run black lambda_cache --check -------------------------------------------------------------------------------- /.github/workflows/misc_items/example_event_release.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "published", 3 | "release": { 4 | "url": "https://api.github.com/repos/Codertocat/Hello-World/releases/17372790", 5 | "assets_url": "https://api.github.com/repos/Codertocat/Hello-World/releases/17372790/assets", 6 | "upload_url": "https://uploads.github.com/repos/Codertocat/Hello-World/releases/17372790/assets{?name,label}", 7 | "html_url": "https://github.com/Codertocat/Hello-World/releases/tag/0.0.1", 8 | "id": 17372790, 9 | "node_id": "MDc6UmVsZWFzZTE3MzcyNzkw", 10 | "tag_name": "0.0.1", 11 | "target_commitish": "master", 12 | "name": null, 13 | "draft": false, 14 | "author": { 15 | "login": "Codertocat", 16 | "id": 21031067, 17 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 18 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/Codertocat", 21 | "html_url": "https://github.com/Codertocat", 22 | "followers_url": "https://api.github.com/users/Codertocat/followers", 23 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 27 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 28 | "repos_url": "https://api.github.com/users/Codertocat/repos", 29 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "prerelease": false, 35 | "created_at": "2019-05-15T15:19:25Z", 36 | "published_at": "2019-05-15T15:20:53Z", 37 | "assets": [ 38 | 39 | ], 40 | "tarball_url": "https://api.github.com/repos/Codertocat/Hello-World/tarball/0.0.1", 41 | "zipball_url": "https://api.github.com/repos/Codertocat/Hello-World/zipball/0.0.1", 42 | "body": null 43 | }, 44 | "repository": { 45 | "id": 186853002, 46 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", 47 | "name": "Hello-World", 48 | "full_name": "Codertocat/Hello-World", 49 | "private": false, 50 | "owner": { 51 | "login": "Codertocat", 52 | "id": 21031067, 53 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 54 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 55 | "gravatar_id": "", 56 | "url": "https://api.github.com/users/Codertocat", 57 | "html_url": "https://github.com/Codertocat", 58 | "followers_url": "https://api.github.com/users/Codertocat/followers", 59 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 60 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 61 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 62 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 63 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 64 | "repos_url": "https://api.github.com/users/Codertocat/repos", 65 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 66 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 67 | "type": "User", 68 | "site_admin": false 69 | }, 70 | "html_url": "https://github.com/Codertocat/Hello-World", 71 | "description": null, 72 | "fork": false, 73 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 74 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 75 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 76 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 77 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 78 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 79 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 80 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 81 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 82 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 83 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 84 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 85 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 86 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 87 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 88 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 89 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 90 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 91 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 92 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 93 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 94 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 95 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 96 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 97 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 98 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 99 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 100 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 101 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 102 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 103 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 104 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 105 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 106 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 107 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 108 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 109 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 110 | "created_at": "2019-05-15T15:19:25Z", 111 | "updated_at": "2019-05-15T15:20:41Z", 112 | "pushed_at": "2019-05-15T15:20:52Z", 113 | "git_url": "git://github.com/Codertocat/Hello-World.git", 114 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 115 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 116 | "svn_url": "https://github.com/Codertocat/Hello-World", 117 | "homepage": null, 118 | "size": 0, 119 | "stargazers_count": 0, 120 | "watchers_count": 0, 121 | "language": "Ruby", 122 | "has_issues": true, 123 | "has_projects": true, 124 | "has_downloads": true, 125 | "has_wiki": true, 126 | "has_pages": true, 127 | "forks_count": 1, 128 | "mirror_url": null, 129 | "archived": false, 130 | "disabled": false, 131 | "open_issues_count": 2, 132 | "license": null, 133 | "forks": 1, 134 | "open_issues": 2, 135 | "watchers": 0, 136 | "default_branch": "master" 137 | }, 138 | "sender": { 139 | "login": "Codertocat", 140 | "id": 21031067, 141 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 142 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 143 | "gravatar_id": "", 144 | "url": "https://api.github.com/users/Codertocat", 145 | "html_url": "https://github.com/Codertocat", 146 | "followers_url": "https://api.github.com/users/Codertocat/followers", 147 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 148 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 149 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 150 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 151 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 152 | "repos_url": "https://api.github.com/users/Codertocat/repos", 153 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 154 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 155 | "type": "User", 156 | "site_admin": false 157 | } 158 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # Triggers 4 | on: 5 | release: 6 | types: [published] 7 | 8 | # Specify what jobs to run 9 | jobs: 10 | Build_Release: 11 | name: Build_Release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Set up Python 3.6 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.6 21 | 22 | # version 1.2 23 | - name: Install Poetry 24 | uses: dschep/install-poetry-action@db2e37f48d1b1cd1491c4590338ebc7699adb425 25 | 26 | # We do not need dependencies for build or publish (native Poetry) 27 | # - name: Install Dependencies 28 | # run: poetry install 29 | 30 | - name: Build 31 | working-directory: . 32 | run: poetry build 33 | 34 | - name: Archive production artifacts 35 | uses: actions/upload-artifact@v1 36 | with: 37 | name: dist 38 | path: dist 39 | 40 | - name: publish 41 | working-directory: . 42 | run: | 43 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 44 | poetry publish 45 | 46 | # Success / Failure, publish to Slack 47 | - name: Notify slack success 48 | if: success() 49 | env: 50 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 51 | uses: voxmedia/github-action-slack-notify-build@51259479e07a6e3c44b826b39b771be8106142ba 52 | with: 53 | channel: lambda-cache 54 | status: SUCCESS 55 | color: good 56 | 57 | - name: Notify slack fail 58 | if: failure() 59 | env: 60 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 61 | uses: voxmedia/github-action-slack-notify-build@51259479e07a6e3c44b826b39b771be8106142ba 62 | with: 63 | channel: lambda-cache 64 | status: FAILED 65 | color: danger -------------------------------------------------------------------------------- /.github/workflows/serverless_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test Function Deploy 2 | 3 | # Triggers 4 | on: 5 | push: 6 | branches: 7 | - 'serverless*' 8 | - 'release' 9 | 10 | # Specify what jobs to run 11 | jobs: 12 | sls_deploy: 13 | name: deploy 14 | runs-on: ubuntu-latest 15 | env: 16 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 17 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 18 | AWS_DEFAULT_REGION: us-east-1 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | 23 | # Install SLS 24 | - name: Serverless Install 25 | working-directory: ./tests/integration_tests 26 | run: npm install serverless@1.54.0 27 | 28 | # Deploy Lambdas 29 | - name: Serverless Deploy 30 | working-directory: ./tests/integration_tests 31 | run: npx serverless deploy -------------------------------------------------------------------------------- /.github/workflows/version_push.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # Triggers 4 | on: 5 | push: 6 | branches-ignore: 7 | - 'docs*' 8 | - 'no_build*' 9 | - master 10 | 11 | # Specify what jobs to run 12 | jobs: 13 | TestBuildDeploy: 14 | name: Test, Lint, Build and Deploy 15 | runs-on: ubuntu-latest 16 | env: 17 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 18 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 19 | AWS_DEFAULT_REGION: us-east-1 20 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | 25 | - name: Set up Python 3.6 26 | uses: actions/setup-python@v1 27 | with: 28 | python-version: 3.6 29 | 30 | - name: Install Poetry 31 | uses: dschep/install-poetry-action@db2e37f48d1b1cd1491c4590338ebc7699adb425 32 | 33 | - name: Install Dependencies 34 | run: poetry install 35 | 36 | - name: Linting with Black 37 | run: poetry run black lambda_cache --check 38 | 39 | - name: Testing with Pytest 40 | working-directory: ./tests 41 | run: poetry run pytest . --cov ../lambda_cache --cov-report term-missing --ignore=./integration_tests/ -vv 42 | 43 | # Only runs on release branch 44 | - name: Integration Test 45 | if: github.ref == 'refs/heads/release' 46 | working-directory: ./tests/integration_tests 47 | run: poetry run pytest -vv 48 | 49 | - name: Update Test Coverage 50 | if: github.ref == 'refs/heads/release' # only on the feature branch do we publish code coverage stats 51 | working-directory: ./tests 52 | run: poetry run coveralls 53 | 54 | # Success / Failure, publish to Slack 55 | - name: Notify slack success 56 | if: success() 57 | env: 58 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 59 | uses: voxmedia/github-action-slack-notify-build@51259479e07a6e3c44b826b39b771be8106142ba 60 | with: 61 | channel: lambda-cache 62 | status: SUCCESS 63 | color: good 64 | 65 | - name: Notify slack fail 66 | if: failure() 67 | env: 68 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 69 | uses: voxmedia/github-action-slack-notify-build@51259479e07a6e3c44b826b39b771be8106142ba 70 | with: 71 | channel: lambda-cache 72 | status: FAILED 73 | color: danger 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | *.egg-info 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Pycharm 133 | .idea 134 | 135 | # vs code 136 | .vscode 137 | 138 | # Terraform 139 | .terraform/ 140 | *.tfstate 141 | 142 | # serverless 143 | .serverless/ 144 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: local 2 | hooks: 3 | - id: Lint 4 | name: Lint with Black 5 | entry: ./.github/pre-commit/lint.sh 6 | language: script 7 | files: ^lambda_cache/ 8 | types: [file, python] -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation with MkDocs 9 | mkdocs: 10 | configuration: mkdocs.yml 11 | 12 | # Optionally build your docs in additional formats such as PDF and ePub 13 | formats: all -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at keith+lambdacache@keithrozario.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This package started of as a weekend project for me, but I am happy to take pull requests from anyone. 2 | 3 | Will gladly put your name on the repor, or contributors.MD file when it happens. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Lambda Cache

2 |

Simple Caching for Lambda Functions

3 | 4 | ![PackageStatus](https://img.shields.io/pypi/status/lambda-cache) ![PyPI version](https://img.shields.io/pypi/v/lambda-cache) ![Downloads](https://img.shields.io/pypi/dw/lambda-cache) 5 | 6 | ![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7%20|%203.8&color=blue?style=flat-square&logo=python) ![License: MIT](https://img.shields.io/github/license/keithrozario/lambda-cache) ![Documentation Status](https://readthedocs.org/projects/lambda-cache/badge/?version=latest) 7 | 8 | ![Test](https://github.com/keithrozario/lambda-cache/workflows/Test/badge.svg?branch=release) [![Coverage Status](https://coveralls.io/repos/github/keithrozario/lambda-cache/badge.svg?branch=release)](https://coveralls.io/github/keithrozario/lambda-cache?branch=release) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ad70a44cb3e54d7ba600edc5fa89635c)](https://www.codacy.com/manual/keithrozario/lambda-cache?utm_source=github.com&utm_medium=referral&utm_content=keithrozario/lambda-cache&utm_campaign=Badge_Grade) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | 10 | # Introduction 11 | 12 |

13 | 14 | _lambda-cache_ helps you cache data in your Lambda function from one invocation to another. It utilizes the internal memory of the lambda function's [execution context](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html) to store data across multiple invocations, which: 15 | 16 | * Reduces load on back-end systems 17 | * Reduces the execution time of the lambda 18 | * Guarantees that functions will reference latest data after cache expiry 19 | 20 | _lambda-cache_ is purpose-built for AWS Lambda functions, and prioritizes simplicity as our design gaol. It currently supports SSM Parameters, Secrets from Secrets Manager and S3 Objects. 21 | 22 | # Installation 23 | 24 | Include the package in your function zip-file artifact using: 25 | 26 | $ pip install lambda-cache -t /path/of/function 27 | 28 | Refer to [installation guide](https://lambda-cache.readthedocs.io/en/latest/install/) for other options. 29 | 30 | # Usage 31 | 32 | The official [user guide](https://lambda-cache.readthedocs.io/en/latest/user_guide/) has more info. 33 | 34 | 35 | * [SSM - Parameter Store](#SSM-ParameterStore) 36 | * [Cache single parameter](#Cachesingleparameter) 37 | * [Change cache expiry](#Changecacheexpiry) 38 | * [Change cache entry settings](#Changecacheentrysettings) 39 | * [Cache multiple parameters](#Cachemultipleparameters) 40 | * [Decorator stacking](#Decoratorstacking) 41 | * [Cache invalidation](#Cacheinvalidation) 42 | * [Return Values](#ReturnValues) 43 | * [Secrets Manager](#SecretsManager) 44 | * [Cache single secret](#Cachesinglesecret) 45 | * [Change Cache expiry](#ChangeCacheexpiry) 46 | * [Change Cache entry settings](#ChangeCacheentrysettings) 47 | * [Decorator stacking](#Decoratorstacking-1) 48 | * [Cache Invalidation](#CacheInvalidation) 49 | * [Return Values](#ReturnValues-1) 50 | * [S3](#S3) 51 | * [Cache a single file](#Cacheasinglefile) 52 | * [Change Cache expiry](#ChangeCacheexpiry-1) 53 | * [Check file before download](#Checkfilebeforedownload) 54 | 55 | ## SSM - Parameter Store 56 | 57 | ### Cache single parameter 58 | 59 | To cache a parameter from ssm, decorate your handler function like so: 60 | 61 | ```python 62 | from lambda_cache import ssm 63 | 64 | @ssm.cache(parameter='/production/app/var') 65 | def handler(event, context): 66 | var = getattr(context,'var') 67 | response = do_something(var) 68 | return response 69 | ``` 70 | All invocations of this function over in the next minute will reference the parameter from the function's internal cache, rather than making a network call to ssm. After one minute has lapsed, the the next invocation will invoke `get_parameter` to refresh the cache. The parameter value will be injected into the `context` object of your lambda handler for retrieval. 71 | 72 | ### Change cache expiry 73 | 74 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines the maximum age of a parameter that is acceptable to the handler function. Cache entries older than this, will be refreshed. To set a longer cache duration (e.g 5 minutes), change the setting: 75 | 76 | ```python 77 | from lambda_cache import ssm 78 | 79 | @ssm.cache(parameter='/production/app/var', max_age_in_seconds=300) 80 | def handler(event, context): 81 | var = getattr(context,'var') 82 | response = do_something(var) 83 | return response 84 | ``` 85 | 86 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the parameter, unless explicitly refreshed using `get_entry` method (described later). The library is primary interested in caching 'across' invocations rather than 'within' an invocation_ 87 | 88 | ### Change cache entry settings 89 | 90 | The default name of the parameter is the string after the last slash('/') character of its name. This means `/production/app/var` and `test/app/var` both resolve to just `var`. To over-ride this default, use the `entry_name` setting like so: 91 | 92 | ```python 93 | from lambda_cache import ssm 94 | 95 | @ssm.cache(parameter='/production/app/var', entry_name='new_var') 96 | def handler(event, context): 97 | var = getattr(context,'new_var') 98 | response = do_something(var) 99 | return response 100 | ``` 101 | 102 | ### Cache multiple parameters 103 | 104 | To cache multiple entries at once, pass a list of parameters to the parameter argument. This method groups all the parameter values in one python dictionary, stored in the Lambda Context under the `entry_name`. 105 | 106 | _Note: When using this method, `entry_name` is a required parameter, if not present a `NoEntryNameError` exception is thrown._ 107 | 108 | ```python 109 | from lambda_cache import ssm 110 | 111 | @ssm.cache(parameter=['/app/var1', '/app/var2'], entry_name='parameters') 112 | def handler(event, context): 113 | var1 = getattr(context,'parameters').get('var1') 114 | var2 = getattr(context,'parameters').get('var2') 115 | response = do_something(var) 116 | return response 117 | ``` 118 | 119 | Under the hood, we use the `get_parameters` API call for boto3, which translate to a single network call for multiple parameters. You can group all parameters types in a single call, including `String`, `StringList` and `SecureString`. `StringList` will return as a list, while all other types will return as plain-text strings. The library does not support returning `SecureString` parameters in encrypted form, and will only return plain-text strings regardless of String type. 120 | 121 | _Note: for this method to work, ensure you have both `ssm:GetParameter` **and** `ssm:GetParameters` (with the 's' at the end) in your function's permission policy_ 122 | 123 | ### Decorator stacking 124 | 125 | If you wish to cache multiple parameters with different expiry times, stack the decorators. In this example, `var1` will be refreshed every 30 seconds, `var2` will be refreshed after 60. 126 | 127 | ```python 128 | @ssm.cache(parameter='/production/app/var1', max_age_in_seconds=30) 129 | @ssm.cache(parameter='/production/app/var2', max_age_in_seconds=60) 130 | def handler(event, context): 131 | var1 = getattr(context,'var1') 132 | var2 = getattr(context,'var2') 133 | response = do_something(var) 134 | return response 135 | ``` 136 | _Note: Decorator stacking performs one API call per decorator, which might result is slower performance_ 137 | 138 | ### Cache invalidation 139 | 140 | If you require a fresh value at some point of the code, you can force a refresh using the `ssm.get_entry` function, and setting the `max_age_in_seconds` argument to 0. 141 | 142 | ```python 143 | from lambda_cache import ssm 144 | 145 | @ssm.cache(parameter='/prod/var') 146 | def handler(event, context): 147 | 148 | if event.get('refresh'): 149 | # refresh parameter 150 | var = ssm.get_entry(parameter='/prod/var', max_age_in_seconds=0) 151 | else: 152 | var = getattr(context,'var') 153 | 154 | response = do_something(var) 155 | return response 156 | ``` 157 | 158 | You may also use `ssm.get_entry` to get a parameter entry from anywhere in your functions code. 159 | 160 | To only get parameter once in the lifetime of the function, set `max_age_in_seconds` to some arbitary large number ~36000 (10 hours). 161 | 162 | ### Return Values 163 | 164 | Caching supports `String`, `SecureString` and `StringList` parameters with no change required (ensure you have `kms:Decrypt` permission for `SecureString`). For simplicity, `StringList` parameters are automatically converted into list (delimited by comma), while `String` and `SecureString` both return the single string value of the parameter. 165 | 166 | ## Secrets Manager 167 | 168 | ### Cache single secret 169 | 170 | Secret support is similar, but uses the `secret.cache` decorator. 171 | 172 | ```python 173 | from lambda_cache import secrets_manager 174 | 175 | @secrets_manager.cache(name='/prod/db/conn_string') 176 | def handler(event, context): 177 | conn_string = getattr(context,'conn_string') 178 | return context 179 | ``` 180 | 181 | 182 | ### Change Cache expiry 183 | 184 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines how long a parameter should be kept in cache before it is refreshed from ssm. To configure longer or shorter times, modify this argument like so: 185 | 186 | ```python 187 | from lambda_cache import secrets_manager 188 | 189 | @secrets_manager.cache(name='/prod/db/conn_string', max_age_in_seconds=300) 190 | def handler(event, context): 191 | var = getattr(context,'conn_string') 192 | response = do_something(var) 193 | return response 194 | ``` 195 | 196 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the parameter, unless explicitly refreshed using get_cache_ssm. The library is primary interested in caching 'across' invocation rather than 'within' an invocation_ 197 | 198 | ### Change Cache entry settings 199 | 200 | The name of the secret is simply shortened to the string after the last slash('/') character of the secret's name. This means `/prod/db/conn_string` and `/test/db/conn_string` resolve to just `conn_string`. To over-ride this default, use `entry_name`: 201 | 202 | ```python 203 | from lambda_cache import secrets_manager 204 | 205 | @secrets_manager.cache(name='/prod/db/conn_string', entry_name='new_var') 206 | def handler(event, context): 207 | var = getattr(context,'new_var') 208 | response = do_something(var) 209 | return response 210 | ``` 211 | 212 | ### Decorator stacking 213 | 214 | If you wish to cache multiple secrets, you can use decorator stacking. 215 | 216 | ```python 217 | @secrets_manager.cache(name='/prod/db/conn_string', max_age_in_seconds=30) 218 | @secrets_manager.cache(name='/prod/app/elk_username_password', max_age_in_seconds=60) 219 | def handler(event, context): 220 | var1 = getattr(context,'conn_string') 221 | var2 = getattr(context,'elk_username_password') 222 | response = do_something(var) 223 | return response 224 | ``` 225 | 226 | _Note: Decorator stacking performs one API call per decorator, which might result is slower performance._ 227 | 228 | ### Cache Invalidation 229 | 230 | To invalidate a secret, use the `get_entry`, setting the `max_age_in_seconds=0`. 231 | ```python 232 | from lambda_cache import secrets_manager 233 | 234 | @secrets_manager.cache(name='/prod/db/conn_string') 235 | def handler(event, context): 236 | 237 | try: 238 | response = db_connect() 239 | except AuthenticationError: 240 | var = secrets_manager.get_entry(name='/prod/db/conn_string', max_age_in_seconds=0) 241 | response = db_connect() 242 | 243 | return response 244 | ``` 245 | 246 | ### Return Values 247 | 248 | Secrets Manager supports both string and binary secrets. For simplicity we will cache the secret in the format it is stored. It is up to the calling application to process the return as Binary or Strings. 249 | 250 | ## S3 251 | 252 | S3 support is considered _experimental_ for now, but withing the python community we see a lot of folks pull down files from S3 for use in AI/ML models. 253 | 254 | Files downloaded from s3 are automatically stored in the `/tmp` directory of the lambda function. This is the only writable directory within lambda, and has a 512MB of storage space. 255 | 256 | ### Cache a single file 257 | To download a file from S3 use the the same decorator pattern: 258 | 259 | ```python 260 | from lambda_cache import s3 261 | 262 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json') 263 | def s3_download_entry_name(event, context): 264 | 265 | # Object from S3 automatically saved to /tmp directory 266 | with open("/tmp/object.json") as file_data: 267 | status = json.loads(file_data.read())['status'] 268 | 269 | return status 270 | ``` 271 | 272 | ### Change Cache expiry 273 | 274 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines how long a file should be kept in `/tmp` before it is refreshed from S3. To configure longer or shorter times, modify this argument like so: 275 | 276 | ```python 277 | from lambda_cache import s3 278 | 279 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json', max_age_in_seconds=300) 280 | def s3_download_entry_name(event, context): 281 | with open("/tmp/object.json") as file_data: 282 | status = json.loads(file_data.read())['status'] 283 | 284 | return status 285 | ``` 286 | 287 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the object, unless explicitly refreshed using `s3.get_entry`. The library is primary interested in caching 'across' invocation rather than 'within' an invocation_ 288 | 289 | ### Check file before download 290 | 291 | By default, _lambda_cache_ will download the file once at cache has expired, however, to save on network bandwidth (and possibly time), we can set the `check_before_download` parameter to True. This will check the age of the object in S3 and download **only** if the object has changed since the last download. 292 | 293 | ```python 294 | from lambda_cache import s3 295 | 296 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json', max_age_in_seconds=300, check_before_download=True) 297 | def s3_download_entry_name(event, context): 298 | with open("/tmp/object.json") as file_data: 299 | status = json.loads(file_data.read())['status'] 300 | 301 | return status 302 | ``` 303 | 304 | _Note: we use the GetHead object call to verify the objects `last_modified_date`. This simplifies the IAM policy of the function, as it still only requires the `s3:GetObject` permission. However, this is still a GET requests, and will be charged as such, for smaller objects it might be cheaper to just download the object_ 305 | 306 | # Credit 307 | 308 | Project inspired by: 309 | * [SSM-Cache](https://github.com/alexcasalboni/ssm-cache-python) 310 | * [middy](https://github.com/middyjs/middy) 311 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design Decision 2 | 3 | ## Where to store cached objects? 4 | 5 | There's two possible locations to store cache objects within an execution context -- that will survive across invocations: 6 | 7 | * The `/tmp` directory 8 | * A `global` variable 9 | 10 | Both aren't perfect, but between the two we chose `global` variables for two reasons: 11 | * They exists only within the context of the application. 12 | * They are faster than files on the file-system, as they don't require reading in or parsing out. 13 | 14 | ## Where to insert cached object? 15 | 16 | We use a decorator construct to inject a cached object into the Lambda Context of the Lambda function. This is the simplest way to ensure the cache entry object is checked at every invocation. 17 | 18 | But where to insert the cache entry? 19 | 20 | There's 4 options: 21 | 22 | * Into Environment Variables of the Lamdba function 23 | * Into the `event` dict of the Lambda function 24 | * Into the `context` object of the Lambda function 25 | * Into a newly created `cache` object of the Lambda function 26 | 27 | [Middy](https://middy.js.org/docs/middlewares.html#ssm) by default uses environment variables, but provides an alternative for users to use the `context` instead. 28 | 29 | Environment variables make sense, and work. But, they're limited in their ability to store variables of type list of dictionaries and leaves a lot of work to the user to parse them as such. Additionally, as a matter of 'purity' these variables should be static for the lifetime of the execution context -- we are all practical, but environment variables are less easy to work with, hence we discard this option. 30 | 31 | The `event` dictionary is a perfect place for the injecting the cache object. After all, it's the place your functions looks for most of it's data. But, the event object is dynamic in nature (depending on the trigger on your lambda), and we run the risk that a cache entry might over-write existing legitimate `event` data, causing hard to trouble-shoot issues. 32 | 33 | Creating a new argument called cache, gives us a guarantee that the cache won't interfere with existing entries. like so: 34 | 35 | ```python 36 | @secrets_manager.cache(secret_name) 37 | def call_with_entry_name(event, context, cache): 38 | secret = cache.get(secret_name) 39 | return context 40 | ``` 41 | But this approach breaks the current norm of handlers accepting just two arguments (event and context). It looks like slightly less elegant, and might not be compatible with future python runtimes. 42 | 43 | Finally, the `context` object provides the following benefits: 44 | * It already exists in the handlers arguments 45 | * Its attributes and methods are fairly static. Risk of over-write is low. 46 | * Variables here can take the form of any python type (str, int, list, dict...) 47 | 48 | Because simplicity drives our design, the `context` object was chosen, as it provided the least friction for developers to use to the package, low risk of cache entry over-writing something, and it could natively handle any pythong type. 49 | 50 | ### Further Reading 51 | 52 | * [AWS Lambda Context](https://docs.aws.amazon.com/lambda/latest/dg/python-context.html) 53 | * [Reverse Engineering Lambda](https://www.denialof.services/lambda/) 54 | * [bootstrap.py](https://gist.github.com/mshivaramie22/662eda1cbe63bf5716ffe5cc8a02811f) 55 | 56 | -------------------------------------------------------------------------------- /docs/images/installed_package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithrozario/lambda-cache/13b6c8e4f04697ae52a170fa5ec0f44e8b29aee2/docs/images/installed_package.png -------------------------------------------------------------------------------- /docs/images/lambda_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithrozario/lambda-cache/13b6c8e4f04697ae52a170fa5ec0f44e8b29aee2/docs/images/lambda_cache.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # lambda-cache 2 | 3 | Simple caching for AWS Lambda. 4 | 5 | The goal of the package is to provide a simple interface for caching, built specifically for AWS Lambda. 6 | 7 | ## Simple use-case 8 | 9 | Our goal is to enable Lambda functions to cache data internally for a period of time, without incurring the delay of making a large number of network calls: 10 | 11 | ![Screenshot](images/lambda_cache.png) 12 | 13 | To do so via code, we decorate our handler method with the `ssm.cache` function: 14 | 15 | ```python 16 | from lambda_cache import ssm 17 | 18 | @ssm.cache(parameter='/production/app/var') 19 | def handler(event, context): 20 | var = getattr(context, 'var') 21 | response = do_something(var) 22 | return response 23 | ``` 24 | 25 | All invocations after the first, will reference the parameter from the function's internal cache, without making a network call to ssm (which incurs a ~50ms delay). After 60 seconds has lapsed, the next invocation will get the latest value from SSM. 26 | 27 | This increases the functions performance, reduces the load on back-end services, and guarantees that invocations after a set-time will begin using the latest parameter value. 28 | 29 | ## Caching Basics 30 | 31 | When a Lambda function is invoked, AWS Lambda launches a temporary environment called an [execution context](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), that initializes the function's runtime and code. After the function has executed, AWS Lambda maintains the execution context for some time in anticipation of another invocation. 32 | 33 | This 'warm' execution context _might_ be reused for the next invocation, as it is more efficient than creating a new execution context for every invocation. Re-using an execution context will keep the following: 34 | 35 | * All Objects declared outside the function's handler method. 36 | * Any file within the `/tmp` directory 37 | * Background processes and callbacks initiated by Lambda that did not complete. 38 | 39 | AWS provide no guarantees on how long execution contexts are kept warm before they are discarded. It probably depends on the load on the service at the time. Applicationshave no control over re-using or getting new execution contexts, it is left to AWS. 40 | 41 | Given that, there are only two options to keep an object in the functions memory, across multiple invocations of that same function. 42 | 43 | **Option 1**: Lookup the object outside the function's handler. This method is fast, cheap and causes the least load on back-end services like SSM or Databases. But because we cannot guarantee how long execution contexts are kept warm, an update to an object, for example a parameter in SSM Parameter store might take hours before they effect function invocations (when all warm functions are finally discarded) 44 | 45 | **Option 2**: Lookup the object on every invocation. This method is slow, expensive, and causes high load on backend systems. But it guarantees that an update to an object takes effect immediately. 46 | 47 | _lambda_cache_ provides a 3rd option, by looking up the object at specific frequencies (e.g. once every hour). Thereby still being fast and cheap, but also guarantee that and update will take effect within a set time across all lambda invocations. 48 | 49 | ## Why use lambda_cache 50 | 51 | The philosophy around _lambda_cache_ is to provide a simple interface for developers to start caching within their Lambda functions, with minimal tweaking necessary. Some call it 'opioniated', we call it simple. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Unlike other packages, _lambda_cache_ was designed to operate specifically within an AWS Lambda Function. Hence the installation is slightly more complicated. 4 | 5 | There are two general options to using it. 6 | 7 | ## Manual Installation 8 | 9 | Because _lambda-cache_ is a pure python package, you can manually include it in your lambda function, like so: 10 | 11 | $ pip install lambda-cache -t /path/to/function 12 | 13 | Once installed you will see the following directory structure in your lambda function via the console: 14 | 15 | ![Installed Package](images/installed_package.png) 16 | 17 | ## Using Serverless Framework 18 | 19 | Using [Serverless Framework](https://serverless.com/), and the [serverless-python-requirements](https://serverless.com/plugins/serverless-python-requirements/) plugin, you can include any python package, including _simple_lambda_cache_ into your lambda function. 20 | 21 | simply ensure that _simple_lambda_cache_ is part of your `requirements.txt` file: 22 | 23 | $ pip install lambda-cache 24 | 25 | ## Using the publicly available layer from Klayers 26 | 27 | [Klayers](https://github.com/keithrozario/Klayers) is a project that publishes AWS Lambda Layers for public consumption. A Lambda layer is way to pre-package code for easy deployments into any Lambda function. 28 | 29 | You can 'install' _lambda_cache_ by simply including the latest layer arn in your lambda function. 30 | 31 | For now include any of the following arns as layers in your package, replacing , with your region of choice (e.g. 'ap-southeast-1','us-east-1', etc.) 32 | 33 | arn:aws:lambda::770693421928:layer:Klayers-python38-lambda-cache:1 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | --- 4 | ## SSM 5 | --- 6 | 7 | ### **ssm.cache** 8 | 9 | Decorator function, meant to decorate invocation handler, but can be used to decorate any method within your lambda function. 10 | 11 | ```python 12 | from lambda_cache import ssm 13 | 14 | @ssm.cache(parameter='/prod/app/var', max_age_in_seconds=60, entry_name='simple_name') 15 | def handler(event, context): 16 | var = getattr(context,'simple_name') 17 | response = do_something(var) 18 | return response 19 | ``` 20 | 21 | * **PARAMETERS** 22 | * **parameter** (_str_ or _list_) -- **[REQUIRED]** 23 | * Name of Parameter(s) is SSM Parameter Store. To cache more than one parameter, pass it a list of parameters. e.g. _['/dev/app/var1', 'dev/app/var2']_ 24 | * **max_age_in_seconds** (_int_): -- 25 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 26 | * **entry_name** (_str_): -- 27 | * Name of entry in _context_ object. Required if **parameter** is of type list (**_default: paremeter.split('/')_**) 28 | 29 | ### **ssm.get_entry** 30 | 31 | Get's the value of the ssm parameter from cache (forces a refresh from parameter store if the cache object is older than `max_age_in_seconds`) 32 | 33 | ```python 34 | from lambda_cache import ssm 35 | 36 | def handler(event, context): 37 | var = ssm.get_entry(parameter='/production/app/var', 38 | max_age_in_seconds=300, 39 | entry_name='simple_name') 40 | response = do_something(var) 41 | return response 42 | ``` 43 | 44 | * **PARAMETERS** 45 | * **parameter** (_str_ or _list_) -- **[REQUIRED]** 46 | * Name of Parameter(s) is SSM Parameter Store. To cache more than one parameter, pass it a list of parameters. e.g. _['/dev/app/var1', 'dev/app/var2']_ 47 | * **max_age_in_seconds** (_int_): -- 48 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 49 | * **entry_name** (_str_): -- 50 | * Name of entry in _context_ object. Required if **parameter** is of type list (**_default: paremeter.split('/')_**) 51 | 52 | --- 53 | ## Secrets Manager 54 | --- 55 | 56 | ### **secrets_manager.cache** 57 | 58 | Decorator function, meant to decorate invocation handler, but can be used to decorate any method within your lambda function. 59 | 60 | ```python 61 | @secrets_manager.cache(name='/prod/db/connection_string', max_age_in_seconds=10, entry_name='new_secret') 62 | def call_with_entry_name(event, context): 63 | secret = getattr(context, 'new_secret') 64 | response do_something(secret) 65 | return response 66 | ``` 67 | 68 | * **PARAMETERS** 69 | * **name** (_str_) -- **[REQUIRED]** 70 | * Name of secret in secrets manager 71 | * **max_age_in_seconds** (_int_): -- 72 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 73 | * **entry_name** (_str_): -- 74 | * Name of entry in cache. Required if (**_default: name.split('/')_**) 75 | 76 | ### **secrets_manager.get_entry** 77 | 78 | Get's the value of the secret from cache (forces a refresh from secrets_manager if cache entry is older than `max_age_in_seconds`) 79 | 80 | ```python 81 | @secrets_manager.cache(name='/prod/db/connection_string', max_age_in_seconds=300, entry_name='secret') 82 | def call_with_entry_name(event, context): 83 | 84 | try: 85 | secret = getattr(context, 'secret') 86 | response = do_something(secret) 87 | except AuthenticationError: 88 | new_secret = secrets_manager.get_entry(name='/prod/db/connection_string', 89 | max_age_in_seconds=0, # forces a reset of secret 90 | entry_name='secret') 91 | response = do_something(secret) 92 | 93 | return response 94 | ``` 95 | 96 | * **PARAMETERS** 97 | * **name** (_str_) -- **[REQUIRED]** 98 | * Name of secret in secrets manager 99 | * **max_age_in_seconds** (_int_): -- 100 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 101 | * **entry_name** (_str_): -- 102 | * Name of entry in cache. Required if (**_default: name.split('/')_**) 103 | 104 | --- 105 | ## S3 106 | --- 107 | 108 | ### s3.cache 109 | 110 | Decorator function, meant to decorate invocation handler, but can be used to decorate any method within your lambda function. 111 | 112 | ```python 113 | from lambda_cache import s3 114 | 115 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json', max_age_in_seconds=300) 116 | def s3_download_entry_name(event, context): 117 | with open("/tmp/object.json") as file_data: 118 | status = json.loads(file_data.read())['status'] 119 | 120 | return status 121 | ``` 122 | * **PARAMETERS** 123 | * **s3Uri** (_str_) -- **[REQUIRED]** 124 | * s3Uri of object to download in the form `s3://bucket-name/path/to/object` 125 | * **max_age_in_seconds** (_int_): -- 126 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 127 | * **entry_name** (_str_): -- 128 | * Name of entry in _context_ object. (**_default: s3Uri.split('/')_**) 129 | * **check_before_download** (_bool_): -- 130 | * Check object age before downloading (useful for large objects). Setting this to `True` will cause package to check if the object has been updated since the last cache refresh, and only download the object **if** the object has changed.(**_default: False_**) 131 | 132 | 133 | ### s3.get_entry 134 | 135 | Get's the value of the secret from cache (forces a refresh from secrets_manager if cache entry is older than `max_age_in_seconds`) 136 | 137 | ```python 138 | def test_get_entry(): 139 | file_location = s3.get_entry(s3Uri=f"s3://{bucket_name}/{s3_key}", max_age_in_seconds=5, entry_name=False, check_before_download=True) 140 | with open(file_location, 'r') as file_data: 141 | status = json.loads(file_data.read())['status'] 142 | ``` 143 | 144 | * **PARAMETERS** 145 | * **s3Uri** (_str_) -- **[REQUIRED]** 146 | * s3Uri of object to download in the form `s3://bucket-name/path/to/object` 147 | * **max_age_in_seconds** (_int_): -- 148 | * Maximum age of a cache entry before a refresh (**_default: 60_**) 149 | * **entry_name** (_str_): -- 150 | * Name of entry cache. (**_default: s3Uri.split('/')_**) 151 | * **check_before_download** (_bool_): -- 152 | * Check object age before downloading (useful for large objects). Setting this to `True` will cause package to check if the object has been updated since the last cache refresh, and only download the object **if** the object has changed.(**_default: False_**) -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | _lambda_cache_ prioritizes simplicity over performance and flexibility. The goal of the package is to provide the **simplest** way for developers to cache api calls in their Lambda functions. 4 | 5 | ## SSM - Parameter Store 6 | 7 | ### Cache single parameter 8 | 9 | To cache a parameter from ssm, decorate your handler function: 10 | 11 | ```python 12 | from lambda_cache import ssm 13 | 14 | @ssm.cache(parameter='/production/app/var') 15 | def handler(event, context): 16 | var = getattr(context,'var') 17 | response = do_something(var) 18 | return response 19 | ``` 20 | All invocations of this function over in the next minute will reference the parameter from the function's internal cache, rather than making a network call to ssm. After one minute has lapsed, the the next invocation will invoke `get_parameter` to refresh the cache. 21 | 22 | ### Change cache expiry 23 | 24 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines the maximum age of a parameter that is acceptable to the handler function. Cache entries older than this, will be refreshed. To set a longer cache duration (e.g 5 minutes), change the setting like so: 25 | 26 | ```python 27 | from lambda_cache import ssm 28 | 29 | @ssm.cache(parameter='/production/app/var', max_age_in_seconds=300) 30 | def handler(event, context): 31 | var = getattr(context,'var') 32 | response = do_something(var) 33 | return response 34 | ``` 35 | 36 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the parameter, unless explicitly refreshed using `get_entry` method. The library is primary interested in caching 'across' invocation rather than 'within' an invocation_ 37 | 38 | ### Change cache entry settings 39 | 40 | The default name of the parameter is simply shortened to the string after the last slash('/') character of its name. This means `/production/app/var` and `test/app/var` resolve to just `var`. To over-ride this default, use `entry_name` setting like so: 41 | 42 | ```python 43 | from lambda_cache import ssm 44 | 45 | @ssm.cache(parameter='/production/app/var', entry_name='new_var') 46 | def handler(event, context): 47 | var = getattr(context,'new_var') 48 | response = do_something(var) 49 | return response 50 | ``` 51 | 52 | ### Cache multiple parameters 53 | 54 | To cache multiple entries at once, pass a list of parameters to the parameter argument. This method groups all the parameter value under one python dictionary, stored in the Lambda Context under the `entry_name`. 55 | 56 | _Note: When using this method, `entry_name` is a required parameter, if not present `NoEntryNameError` exception is thrown._ 57 | 58 | ```python 59 | from lambda_cache import ssm 60 | 61 | @ssm.cache(parameter=['/app/var1', '/app/var2'], entry_name='parameters') 62 | def handler(event, context): 63 | var1 = getattr(context,'parameters').get('var1') 64 | var2 = getattr(context,'parameters').get('var2') 65 | response = do_something(var) 66 | return response 67 | ``` 68 | 69 | Under the hood, we use the `get_parameters` API call for boto3, which translate to a single network call for multiple parameters. You can group all parameters types in a single call, including `String`, `StringList` and `SecureString`. `StringList` will return as a list, while all other types will return as plain-text strings. The library does not support returning `SecureString` parameters in encrypted form, and will only return plain-text strings regardless of String type. 70 | 71 | _Note: for this method to work, ensure you have both `ssm:GetParameter` **and** `ssm:GetParameters` (with the 's' at the end) in your function's permission policy_ 72 | 73 | ### Decorator stacking 74 | 75 | If you wish to cache multiple parameters with different expiry times, stack the decorators. In this example, `var1` will be refreshed every 30 seconds, `var2` will be refreshed after 60. 76 | 77 | ```python 78 | @ssm.cache(parameter='/production/app/var1', max_age_in_seconds=30) 79 | @ssm.cache(parameter='/production/app/var2', max_age_in_seconds=60) 80 | def handler(event, context): 81 | var1 = getattr(context,'var1') 82 | var2 = getattr(context,'var2') 83 | response = do_something(var) 84 | return response 85 | ``` 86 | _Note: Decorator stacking performs one API call per decorator, which might result is slower performance_ 87 | 88 | ### Cache invalidation 89 | 90 | If you require a fresh value at some point of the code, you can force a refresh using the `ssm.get_entry` function, and setting the `max_age_in_seconds` argument to 0. 91 | 92 | ```python 93 | from lambda_cache import ssm 94 | 95 | @ssm.cache(parameter='/prod/var') 96 | def handler(event, context): 97 | 98 | if event.get('refresh'): 99 | # refresh parameter 100 | var = ssm.get_entry(parameter='/prod/var', max_age_in_seconds=0) 101 | else: 102 | var = getattr(context,'var') 103 | 104 | response = do_something(var) 105 | return response 106 | ``` 107 | 108 | You may also use `ssm.get_entry` to get a parameter entry from anywhere in your functions code. 109 | 110 | To only get parameter once in the lifetime of the function, set `max_age_in_seconds` to some arbitary large number ~36000 (10 hours). 111 | 112 | ### Return Values 113 | 114 | Caching supports `String`, `SecureString` and `StringList` parameters with no change required (ensure you have `kms:Decrypt` permission for `SecureString`). For simplicity, `StringList` parameters are automatically converted into list (delimited by comma), while `String` and `SecureString` both return the single string value of the parameter. 115 | 116 | ## Secrets Manager 117 | 118 | ### Cache single secret 119 | 120 | Secret support is similar, but uses the `secret.cache` decorator. 121 | 122 | ```python 123 | from lambda_cache import secrets_manager 124 | 125 | @secrets_manager.cache(name='/prod/db/conn_string') 126 | def handler(event, context): 127 | conn_string = getattr(context,'conn_string') 128 | return context 129 | ``` 130 | 131 | 132 | ### Change Cache expiry 133 | 134 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines how long a parameter should be kept in cache before it is refreshed from ssm. To configure longer or shorter times, modify this argument like so: 135 | 136 | ```python 137 | from lambda_cache import secrets_manager 138 | 139 | @secrets_manager.cache(name='/prod/db/conn_string', max_age_in_seconds=300) 140 | def handler(event, context): 141 | var = getattr(context,'conn_string') 142 | response = do_something(var) 143 | return response 144 | ``` 145 | 146 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the parameter, unless explicitly refreshed using get_cache_ssm. The library is primary interested in caching 'across' invocation rather than 'within' an invocation_ 147 | 148 | ### Change Cache entry settings 149 | 150 | The name of the secret is simply shortened to the string after the last slash('/') character of the secret's name. This means `/prod/db/conn_string` and `/test/db/conn_string` resolve to just `conn_string`. To over-ride this default, use `entry_name`: 151 | 152 | ```python 153 | from lambda_cache import secrets_manager 154 | 155 | @secrets_manager.cache(name='/prod/db/conn_string', entry_name='new_var') 156 | def handler(event, context): 157 | var = getattr(context,'new_var') 158 | response = do_something(var) 159 | return response 160 | ``` 161 | 162 | ### Decorator stacking 163 | 164 | If you wish to cache multiple secrets, you can use decorator stacking. 165 | 166 | ```python 167 | @secrets_manager.cache(name='/prod/db/conn_string', max_age_in_seconds=30) 168 | @secrets_manager.cache(name='/prod/app/elk_username_password', max_age_in_seconds=60) 169 | def handler(event, context): 170 | var1 = getattr(context,'conn_string') 171 | var2 = getattr(context,'elk_username_password') 172 | response = do_something(var) 173 | return response 174 | ``` 175 | 176 | _Note: Decorator stacking performs one API call per decorator, which might result is slower performance._ 177 | 178 | ### Cache Invalidation 179 | 180 | To invalidate a secret, use the `get_entry`, setting the `max_age_in_seconds=0`. 181 | ```python 182 | from lambda_cache import secrets_manager 183 | 184 | @secrets_manager.cache(name='/prod/db/conn_string') 185 | def handler(event, context): 186 | 187 | if event.get('refresh'): 188 | var = secrets_manager.get_entry(name='/prod/db/conn_string', max_age_in_seconds=0) 189 | else: 190 | var = getattr(context,'conn_string') 191 | response = do_something(var) 192 | return response 193 | ``` 194 | 195 | ### Return Values 196 | 197 | Secrets Manager supports both string and binary secrets. For simplicity we will cache the secret in the format it is stored. It is up to the calling application to process the return as Binary or Strings. 198 | 199 | ## S3 200 | 201 | S3 support is considered _experimental_ for now, but withing the python community we see a lot of folks pull down files from S3 for use in AI/ML models. 202 | 203 | Files downloaded from s3 are automatically stored in the `/tmp` directory of the lambda function. This is the only writable directory within lambda, and has a 512MB of storage space. 204 | 205 | ### Cache a single file 206 | To download a file from S3 use the the same decorator pattern: 207 | 208 | 209 | ```python 210 | from lambda_cache import s3 211 | 212 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json') 213 | def s3_download_entry_name(event, context): 214 | with open("/tmp/object.json") as file_data: 215 | status = json.loads(file_data.read())['status'] 216 | 217 | return status 218 | ``` 219 | 220 | ### Change Cache expiry 221 | 222 | The default `max_age_in_seconds` settings is 60 seconds (1 minute), it defines how long a file should be kept in `/tmp` before it is refreshed from S3. To configure longer or shorter times, modify this argument like so: 223 | 224 | ```python 225 | from lambda_cache import s3 226 | 227 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json', max_age_in_seconds=300) 228 | def s3_download_entry_name(event, context): 229 | with open("/tmp/object.json") as file_data: 230 | status = json.loads(file_data.read())['status'] 231 | 232 | return status 233 | ``` 234 | 235 | _Note: The caching logic runs only at invocation, regardless of how long the function runs. A 15 minute lambda function will not refresh the object, unless explicitly refreshed using `s3.get_entry`. The library is primary interested in caching 'across' invocation rather than 'within' an invocation_ 236 | 237 | ### Check file before download 238 | 239 | By default, _lambda_cache_ will download the file once at cache has expired, however, to save on network bandwidth (and possibly time), we can set the `check_before_download` parameter to True. This will check the age of the object in S3 and download **only** if the object has changed since the last download. 240 | 241 | ```python 242 | from lambda_cache import s3 243 | 244 | @s3.cache(s3Uri='s3://bucket_name/path/to/object.json', max_age_in_seconds=300, check_before_download=True) 245 | def s3_download_entry_name(event, context): 246 | with open("/tmp/object.json") as file_data: 247 | status = json.loads(file_data.read())['status'] 248 | 249 | return status 250 | ``` 251 | 252 | _Note: we use the GetHead object call to verify the objects `last_modified_date`. This simplifies the IAM policy of the function, as it still only requires the `s3:GetObject` permission. However, this is still a GET requests, and will be charged as such, for smaller objects it might be cheaper to just download the object_ -------------------------------------------------------------------------------- /lambda_cache/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | lambda-cache 5 | ~~~~~~~~~~~~ 6 | 7 | A python package for caching within AWS Lambda Functions 8 | 9 | Full Documentation is at . 10 | :license: MIT, see LICENSE for more details. 11 | """ 12 | 13 | __version__ = "0.8.1" 14 | 15 | from .ssm import cache, get_entry 16 | from .secrets_manager import cache, get_entry 17 | from .s3 import cache, get_entry 18 | -------------------------------------------------------------------------------- /lambda_cache/caching_logic.py: -------------------------------------------------------------------------------- 1 | import time 2 | import functools 3 | 4 | from .exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 5 | 6 | 7 | def get_decorator(**kwargs): 8 | 9 | """ 10 | Args: 11 | argument (string, list, dict) : argument to be passed to the missed function 12 | max_age_in_seconds(int) : Time to Live of the entry in seconds 13 | entry_name(string) : Name of entry in cache, is also the name of the entry in the event object 14 | miss_function(function): Function to execute when there is a miss on the cache or cache is expired 15 | - Any additional kwargs to be passed to miss_function. 16 | return: 17 | Decorator of the function 18 | """ 19 | 20 | def decorator(func): 21 | @functools.wraps(func) 22 | def inner_function(event, context): 23 | 24 | response = check_cache(**kwargs) 25 | 26 | # Inject {parameter_name: parameter_value} into context object 27 | for key in response: 28 | setattr(context, key, response[key]) 29 | 30 | return func(event, context) 31 | 32 | return inner_function 33 | 34 | return decorator 35 | 36 | 37 | def get_value(**kwargs): 38 | 39 | """ 40 | returns value of check_cache. 41 | """ 42 | response = check_cache(**kwargs) 43 | parameter_value = list(response.values())[0] 44 | return parameter_value 45 | 46 | 47 | def check_cache( 48 | argument, 49 | max_age_in_seconds, 50 | entry_name, 51 | miss_function, 52 | send_details=False, 53 | **kwargs 54 | ): 55 | 56 | """ 57 | Executes the caching logic, checks cache for entry 58 | If entry doesn't exist, returns entry_value by calling the miss function with entry_name and var_name 59 | If entry does exist check entry_age: 60 | If entry_age < max_age_in_seconds, returns value from cache 61 | If entry_age >= max_age_in_seconds, returns value by calling miss_function 62 | 63 | Args: 64 | argument (string, list, dict) : argument to be passed to the missed function 65 | max_age_in_seconds(int) : Time to Live of the entry in seconds 66 | entry_name(string) : Name of entry in cache, is also the name of the entry in the event object 67 | miss_function(function): Function to execute when there is a miss on the cache or cache is expired 68 | Returns: 69 | entry_value(dict) : {entry_name: entry_value} 70 | """ 71 | 72 | entry_name = get_entry_name(argument, entry_name) 73 | entry_age_in_seconds = get_entry_age(entry_name) 74 | 75 | # if kwargs exist, then pass additional data to miss_function, else just argument 76 | if send_details: 77 | kwargs["argument"] = argument 78 | kwargs["entry_name"] = entry_name 79 | kwargs["entry_age_in_seconds"] = entry_age_in_seconds 80 | kwargs["max_age_in_seconds"] = max_age_in_seconds 81 | 82 | if entry_age_in_seconds is None: 83 | if send_details: 84 | entry_value = miss_function(**kwargs) 85 | else: 86 | entry_value = miss_function(argument) 87 | update_cache(entry_name, entry_value) 88 | 89 | elif entry_age_in_seconds < max_age_in_seconds: 90 | entry_value = get_entry_from_cache(entry_name) 91 | 92 | else: 93 | if send_details: 94 | entry_value = miss_function(**kwargs) 95 | else: 96 | entry_value = miss_function(argument) 97 | update_cache(entry_name, entry_value) 98 | 99 | return {entry_name: entry_value} 100 | 101 | 102 | def get_entry_name(argument, entry_name): 103 | 104 | """ 105 | argument is either SSM Parameter, Secret in Secrets Manager or Key in S3 bucket: 106 | SSM Parameter names can include only the following symbols and letters: a-zA-Z0-9_.-/ 107 | Secret name must be ASCII letters, digits, or the following characters : /_+=.@- 108 | S3 Keys can have a varied characters 109 | 110 | if entry_name is set, we return entry_name 111 | if entry_name is False, 112 | if entry_name is a string, Default entry_name to the string after the last '/' in argument 113 | 114 | Args: 115 | argument (string, list, dict) : argument to be passed to the missed function 116 | entry_name(string) : Optional name of entry in cache, and variable injected into event object 117 | Returns: 118 | cache_entry_name : Name of Entry in the cache 119 | """ 120 | 121 | if isinstance(argument, str): 122 | if entry_name: 123 | cache_entry_name = entry_name 124 | else: 125 | cache_entry_name = argument.split("/")[-1] 126 | 127 | elif type(argument) in [int, list, dict]: 128 | if not entry_name: 129 | raise NoEntryNameError( 130 | "You must specify an entry_name for arguments of type list, dict or int" 131 | ) 132 | else: 133 | cache_entry_name = entry_name 134 | else: 135 | raise ArgumentTypeNotSupportedError( 136 | "Argument can only be of Type str, int, list or dict" 137 | ) 138 | 139 | return cache_entry_name 140 | 141 | 142 | def get_entry_age(entry_name): 143 | 144 | """ 145 | Args: 146 | entry_name(string): Name of entry to get age for 147 | Returns: 148 | entry_age_seconds(int): Age of entry in seconds, returns None if no entry exist 149 | """ 150 | global global_aws_lambda_cache 151 | 152 | try: 153 | get_param_timestamp = global_aws_lambda_cache[entry_name][ 154 | "last_updated_timestamp" 155 | ] 156 | entry_age = int(time.time() - get_param_timestamp) 157 | 158 | # cache doesn't exist. Create it. 159 | except NameError: 160 | global_aws_lambda_cache = { 161 | entry_name: {"value": None, "last_updated_timestamp": None} 162 | } 163 | entry_age = None 164 | 165 | # entry doesn't exist in cache or is still None (due to partial failure) 166 | except (KeyError, TypeError): 167 | global_aws_lambda_cache[entry_name] = { 168 | "value": None, 169 | "last_updated_timestamp": None, 170 | } 171 | entry_age = None 172 | 173 | return entry_age 174 | 175 | 176 | def update_cache(entry_name, entry_value): 177 | global global_aws_lambda_cache 178 | 179 | global_aws_lambda_cache[entry_name] = { 180 | "value": entry_value, 181 | "last_updated_timestamp": time.time(), 182 | } 183 | 184 | return 185 | 186 | 187 | def get_entry_from_cache(entry_name): 188 | 189 | """ 190 | Gets entry value from the cache 191 | 192 | Args: 193 | entry_name (string): Name of the entry in cache 194 | Returns: 195 | entry_value (any): Value of entry in cache 196 | """ 197 | 198 | global global_aws_lambda_cache 199 | entry_value = global_aws_lambda_cache.get(entry_name).get("value") 200 | return entry_value 201 | -------------------------------------------------------------------------------- /lambda_cache/exceptions.py: -------------------------------------------------------------------------------- 1 | class LambdaCacheError(Exception): 2 | 3 | """ 4 | Base class for exceptions in this module. 5 | """ 6 | 7 | pass 8 | 9 | 10 | class ArgumentTypeNotSupportedError(LambdaCacheError): 11 | 12 | """ 13 | Raised when Argument is not supported by the function. 14 | """ 15 | 16 | def __init__(self, message): 17 | self.message = message 18 | self.Code = "ArgumentTypeNotSupportedError" 19 | 20 | 21 | class NoEntryNameError(LambdaCacheError): 22 | 23 | """ 24 | Raised when No entry_name is provided. 25 | """ 26 | 27 | def __init__(self, message=False): 28 | self.message = "No entry_name provided" 29 | self.Code = "NoEntryNameError" 30 | 31 | 32 | class InvalidS3UriError(LambdaCacheError): 33 | 34 | """ 35 | s3Uri provided in invalid format 36 | """ 37 | 38 | def __init__(self, invalid_uri): 39 | self.message = f"Expected Valid s3uri of the form 's3://bucket-name/path/to/file', given: {invalid_uri}" 40 | self.Code = "InvalidS3UriError" 41 | -------------------------------------------------------------------------------- /lambda_cache/s3.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from datetime import datetime, timezone 3 | 4 | from .caching_logic import get_decorator, get_value 5 | from .exceptions import InvalidS3UriError 6 | 7 | 8 | def cache( 9 | s3Uri: str, max_age_in_seconds=60, entry_name=False, check_before_download=True 10 | ): 11 | """ 12 | Calls parameter caching, and decorates function by injecting key value into the context object 13 | 14 | Args: 15 | s3Uri(string): S3 Uri of file to download 16 | max_age_in_seconds(int) : Time to Live of the parameter in seconds 17 | entry_name(str) : Name of entry in cache 18 | check_before_download (boolean): Check object age before downloading 19 | Returns: 20 | decorate : Decorated function 21 | 22 | """ 23 | 24 | decorator = get_decorator( 25 | argument=s3Uri, 26 | max_age_in_seconds=max_age_in_seconds, 27 | entry_name=entry_name, 28 | miss_function=get_object_from_s3, 29 | check_before_download=check_before_download, 30 | send_details=True, # specifies to send all details to miss_function 31 | s3Uri=s3Uri, 32 | ) 33 | return decorator 34 | 35 | 36 | def get_entry( 37 | s3Uri: str, max_age_in_seconds=60, entry_name=False, check_before_download=True 38 | ): 39 | """ 40 | Wrapper function for parameter_caching 41 | 42 | Args: 43 | parameter(string): Name of the parameter in System Manager Parameter Store 44 | max_age_in_seconds(int) : Time to Live of the parameter in seconds 45 | var_name(string) : Optional name of parameter to inject into context object 46 | 47 | Returns: 48 | parameter_value(string) : Value of the parameter 49 | """ 50 | 51 | file_location = get_value( 52 | argument=s3Uri, 53 | max_age_in_seconds=max_age_in_seconds, 54 | entry_name=entry_name, 55 | miss_function=get_object_from_s3, 56 | check_before_download=check_before_download, 57 | send_details=True, # specifies to send all details to miss_function 58 | s3Uri=s3Uri, 59 | ) 60 | 61 | return file_location 62 | 63 | 64 | def get_object_from_s3(**kwargs): 65 | 66 | """ 67 | Gets parameter value from the System manager Parameter store 68 | 69 | Args: 70 | kwargs['s3Uri']: Uri of S3 object in the form of s3://bucket-name/path/to/object 71 | kwargs['entry_name']: Name of entry in cache 72 | kwargs['entry_age_in_seconds']: Age of cache entry 73 | Returns: 74 | file_location (str): location of file on file-system (e.g. /tmp/example.txt) 75 | 76 | """ 77 | s3_uri = kwargs["s3Uri"] 78 | bucket_name, key = parse_s3_uri(s3_uri) 79 | file_location = f"/tmp/{kwargs['entry_name']}" 80 | 81 | s3 = boto3.resource("s3") 82 | s3_object = s3.Object(bucket_name, key) 83 | 84 | if kwargs["check_before_download"] and kwargs["entry_age_in_seconds"] is not None: 85 | last_modified = s3_object.last_modified 86 | now = datetime.now(timezone.utc) 87 | object_age_in_seconds = (now - last_modified).seconds 88 | 89 | # if the object is older than the entry_age 90 | if object_age_in_seconds > kwargs["entry_age_in_seconds"]: 91 | return file_location 92 | 93 | s3_object.download_file(file_location) 94 | 95 | return file_location 96 | 97 | 98 | def parse_s3_uri(s3_uri): 99 | 100 | elements = s3_uri.split("/") 101 | 102 | if elements[0] != "s3:" or elements[1] != "": 103 | raise InvalidS3UriError(s3_uri) 104 | else: 105 | bucket_name = elements[2] 106 | key = "/".join(elements[3:]) 107 | 108 | return bucket_name, key 109 | -------------------------------------------------------------------------------- /lambda_cache/secrets_manager.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | from .caching_logic import get_decorator, get_value 4 | from .exceptions import ArgumentTypeNotSupportedError 5 | 6 | default_max_age_in_seconds = 60 7 | 8 | 9 | def cache(name, max_age_in_seconds=default_max_age_in_seconds, entry_name=False): 10 | """ 11 | Calls check cache, and decorates function by injecting key value into the context object 12 | ** The secret name must be ASCII letters, digits, or the following characters : /_+=.@- 13 | Args: 14 | name(string) : Name of the secret (or ARN) 15 | ttl_seconds(int) : Time to Live of the parameter in seconds 16 | var_name(string) : Optional name of parameter to inject into context object 17 | 18 | Returns: 19 | decorate : Decorated function 20 | 21 | """ 22 | 23 | decorator = get_decorator( 24 | argument=name, 25 | max_age_in_seconds=max_age_in_seconds, 26 | entry_name=entry_name, 27 | miss_function=get_secret_from_secrets_manager, 28 | ) 29 | return decorator 30 | 31 | 32 | def get_entry(name, max_age_in_seconds=default_max_age_in_seconds, entry_name=False): 33 | """ 34 | Wrapper function for parameter_caching 35 | 36 | Args: 37 | name(string): Name of the Secret in Secrets Manager (arn is also accepted) 38 | ttl_seconds(int) : Time to Live of the parameter in seconds 39 | var_name(string) : Optional name of parameter to inject into context object 40 | 41 | Returns: 42 | secret_value(string) : Value of the parameter 43 | """ 44 | secret_value = get_value( 45 | argument=name, 46 | max_age_in_seconds=max_age_in_seconds, 47 | entry_name=entry_name, 48 | miss_function=get_secret_from_secrets_manager, 49 | ) 50 | return secret_value 51 | 52 | 53 | def get_secret_from_secrets_manager(name): 54 | """ 55 | Gets parameter value from the System manager Parameter store 56 | 57 | Args: 58 | name(string): Name of the secret or ARN of secret 59 | Returns: 60 | secret_value (string/binary): Value of secret in secrets manager in either String or Binary format 61 | """ 62 | 63 | if isinstance(name, str): 64 | secrets_client = boto3.client("secretsmanager") 65 | response = secrets_client.get_secret_value(SecretId=name) 66 | if response.get("SecretString") is not None: 67 | return_value = response["SecretString"] 68 | else: 69 | return_value = response["SecretBinary"] 70 | else: 71 | raise ArgumentTypeNotSupportedError( 72 | f"Secrets Manager only supports str arguments: {name} is not a string" 73 | ) 74 | 75 | return return_value 76 | -------------------------------------------------------------------------------- /lambda_cache/ssm.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | from .caching_logic import get_decorator, get_value, get_entry_name 4 | from .exceptions import ArgumentTypeNotSupportedError 5 | 6 | default_max_age_in_seconds = 60 7 | 8 | 9 | def cache( 10 | parameter: str, max_age_in_seconds=default_max_age_in_seconds, entry_name=False 11 | ): 12 | """ 13 | Calls parameter caching, and decorates function by injecting key value into the context object 14 | 15 | Args: 16 | parameter(string): Name of the parameter in System Manager Parameter Store 17 | max_age_in_seconds(int) : Time to Live of the parameter in seconds 18 | var_name(string) : Optional name of parameter to inject into context object 19 | 20 | Returns: 21 | decorate : Decorated function 22 | 23 | """ 24 | 25 | decorator = get_decorator( 26 | argument=parameter, 27 | max_age_in_seconds=max_age_in_seconds, 28 | entry_name=entry_name, 29 | miss_function=get_parameter_from_ssm, 30 | ) 31 | return decorator 32 | 33 | 34 | def get_entry( 35 | parameter: str, max_age_in_seconds=default_max_age_in_seconds, entry_name=False 36 | ): 37 | """ 38 | Wrapper function for parameter_caching 39 | 40 | Args: 41 | parameter(string): Name of the parameter in System Manager Parameter Store 42 | max_age_in_seconds(int) : Time to Live of the parameter in seconds 43 | var_name(string) : Optional name of parameter to inject into context object 44 | 45 | Returns: 46 | parameter_value(string) : Value of the parameter 47 | """ 48 | 49 | parameter_value = get_value( 50 | argument=parameter, 51 | max_age_in_seconds=max_age_in_seconds, 52 | entry_name=entry_name, 53 | miss_function=get_parameter_from_ssm, 54 | ) 55 | return parameter_value 56 | 57 | 58 | def get_parameter_from_ssm(parameter): 59 | """ 60 | Gets parameter value from the System manager Parameter store 61 | 62 | Args: 63 | parameter(string / list): Name of the parameter(s) in System Manager Parameter Store 64 | Returns: 65 | parameter_value (string): Single Value of parameter in Parameter Store; or 66 | parameters (list): List of Values from Parameter Store 67 | """ 68 | 69 | ssm_client = boto3.client("ssm") 70 | 71 | if isinstance(parameter, str): 72 | response = ssm_client.get_parameter(Name=parameter, WithDecryption=True) 73 | parameter_value = response["Parameter"]["Value"] 74 | # return StringList 75 | if response["Parameter"]["Type"] == "StringList": 76 | parameter_value = parameter_value.split(",") 77 | return_value = parameter_value 78 | 79 | elif isinstance(parameter, list): 80 | response = ssm_client.get_parameters(Names=parameter, WithDecryption=True) 81 | parameters = {} 82 | for param in response["Parameters"]: 83 | param_name = get_entry_name(param["Name"], False) 84 | if param["Type"] == "StringList": 85 | parameters[param_name] = param["Value"].split(",") 86 | else: 87 | parameters[param_name] = param["Value"] 88 | return_value = parameters 89 | 90 | else: 91 | raise ArgumentTypeNotSupportedError( 92 | "Only str or list supported for ssm, {parameter} is of type: {type(parameter)}" 93 | ) 94 | 95 | return return_value 96 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: lambda cache 2 | nav: 3 | - Home: index.md 4 | - Installation: install.md 5 | - User Guide: user_guide.md 6 | - Reference: reference.md 7 | 8 | theme: readthedocs 9 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "apipkg: namespace control and lazy-import mechanism" 4 | name = "apipkg" 5 | optional = false 6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 7 | version = "1.5" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 12 | name = "appdirs" 13 | optional = false 14 | python-versions = "*" 15 | version = "1.4.3" 16 | 17 | [[package]] 18 | category = "dev" 19 | description = "Atomic file writes." 20 | name = "atomicwrites" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 23 | version = "1.3.0" 24 | 25 | [[package]] 26 | category = "dev" 27 | description = "Classes Without Boilerplate" 28 | name = "attrs" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 31 | version = "19.3.0" 32 | 33 | [package.extras] 34 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 35 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 36 | docs = ["sphinx", "zope.interface"] 37 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 38 | 39 | [[package]] 40 | category = "dev" 41 | description = "The uncompromising code formatter." 42 | name = "black" 43 | optional = false 44 | python-versions = ">=3.6" 45 | version = "19.10b0" 46 | 47 | [package.dependencies] 48 | appdirs = "*" 49 | attrs = ">=18.1.0" 50 | click = ">=6.5" 51 | pathspec = ">=0.6,<1" 52 | regex = "*" 53 | toml = ">=0.9.4" 54 | typed-ast = ">=1.4.0" 55 | 56 | [package.extras] 57 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 58 | 59 | [[package]] 60 | category = "dev" 61 | description = "The AWS SDK for Python" 62 | name = "boto3" 63 | optional = false 64 | python-versions = "*" 65 | version = "1.12.36" 66 | 67 | [package.dependencies] 68 | botocore = ">=1.15.36,<1.16.0" 69 | jmespath = ">=0.7.1,<1.0.0" 70 | s3transfer = ">=0.3.0,<0.4.0" 71 | 72 | [[package]] 73 | category = "dev" 74 | description = "Low-level, data-driven core of boto 3." 75 | name = "botocore" 76 | optional = false 77 | python-versions = "*" 78 | version = "1.15.36" 79 | 80 | [package.dependencies] 81 | docutils = ">=0.10,<0.16" 82 | jmespath = ">=0.7.1,<1.0.0" 83 | python-dateutil = ">=2.1,<3.0.0" 84 | 85 | [package.dependencies.urllib3] 86 | python = "<3.4.0 || >=3.5.0" 87 | version = ">=1.20,<1.26" 88 | 89 | [[package]] 90 | category = "dev" 91 | description = "Python package for providing Mozilla's CA Bundle." 92 | name = "certifi" 93 | optional = false 94 | python-versions = "*" 95 | version = "2019.11.28" 96 | 97 | [[package]] 98 | category = "dev" 99 | description = "Validate configuration and produce human readable error messages." 100 | name = "cfgv" 101 | optional = false 102 | python-versions = ">=3.6.1" 103 | version = "3.1.0" 104 | 105 | [[package]] 106 | category = "dev" 107 | description = "Universal encoding detector for Python 2 and 3" 108 | name = "chardet" 109 | optional = false 110 | python-versions = "*" 111 | version = "3.0.4" 112 | 113 | [[package]] 114 | category = "dev" 115 | description = "Composable command line interface toolkit" 116 | name = "click" 117 | optional = false 118 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 119 | version = "7.1.1" 120 | 121 | [[package]] 122 | category = "dev" 123 | description = "Cross-platform colored terminal text." 124 | marker = "sys_platform == \"win32\" and python_version != \"3.4\"" 125 | name = "colorama" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 128 | version = "0.4.3" 129 | 130 | [[package]] 131 | category = "dev" 132 | description = "Code coverage measurement for Python" 133 | name = "coverage" 134 | optional = false 135 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 136 | version = "5.0.4" 137 | 138 | [package.extras] 139 | toml = ["toml"] 140 | 141 | [[package]] 142 | category = "dev" 143 | description = "Show coverage stats online via coveralls.io" 144 | name = "coveralls" 145 | optional = false 146 | python-versions = "*" 147 | version = "1.11.1" 148 | 149 | [package.dependencies] 150 | coverage = ">=3.6,<6.0" 151 | docopt = ">=0.6.1" 152 | requests = ">=1.0.0" 153 | 154 | [package.extras] 155 | yaml = ["PyYAML (>=3.10,<5.3)"] 156 | 157 | [[package]] 158 | category = "dev" 159 | description = "Distribution utilities" 160 | name = "distlib" 161 | optional = false 162 | python-versions = "*" 163 | version = "0.3.0" 164 | 165 | [[package]] 166 | category = "dev" 167 | description = "Pythonic argument parser, that will make you smile" 168 | name = "docopt" 169 | optional = false 170 | python-versions = "*" 171 | version = "0.6.2" 172 | 173 | [[package]] 174 | category = "dev" 175 | description = "Docutils -- Python Documentation Utilities" 176 | name = "docutils" 177 | optional = false 178 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 179 | version = "0.15.2" 180 | 181 | [[package]] 182 | category = "dev" 183 | description = "execnet: rapid multi-Python deployment" 184 | name = "execnet" 185 | optional = false 186 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 187 | version = "1.7.1" 188 | 189 | [package.dependencies] 190 | apipkg = ">=1.4" 191 | 192 | [package.extras] 193 | testing = ["pre-commit"] 194 | 195 | [[package]] 196 | category = "dev" 197 | description = "A platform independent file lock." 198 | name = "filelock" 199 | optional = false 200 | python-versions = "*" 201 | version = "3.0.12" 202 | 203 | [[package]] 204 | category = "dev" 205 | description = "Clean single-source support for Python 3 and 2" 206 | name = "future" 207 | optional = false 208 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 209 | version = "0.18.2" 210 | 211 | [[package]] 212 | category = "dev" 213 | description = "File identification library for Python" 214 | name = "identify" 215 | optional = false 216 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 217 | version = "1.4.14" 218 | 219 | [package.extras] 220 | license = ["editdistance"] 221 | 222 | [[package]] 223 | category = "dev" 224 | description = "Internationalized Domain Names in Applications (IDNA)" 225 | name = "idna" 226 | optional = false 227 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 228 | version = "2.9" 229 | 230 | [[package]] 231 | category = "dev" 232 | description = "Read metadata from Python packages" 233 | marker = "python_version < \"3.8\"" 234 | name = "importlib-metadata" 235 | optional = false 236 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 237 | version = "1.6.0" 238 | 239 | [package.dependencies] 240 | zipp = ">=0.5" 241 | 242 | [package.extras] 243 | docs = ["sphinx", "rst.linker"] 244 | testing = ["packaging", "importlib-resources"] 245 | 246 | [[package]] 247 | category = "dev" 248 | description = "Read resources from Python packages" 249 | marker = "python_version < \"3.7\"" 250 | name = "importlib-resources" 251 | optional = false 252 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 253 | version = "1.4.0" 254 | 255 | [package.dependencies] 256 | [package.dependencies.importlib-metadata] 257 | python = "<3.8" 258 | version = "*" 259 | 260 | [package.dependencies.zipp] 261 | python = "<3.8" 262 | version = ">=0.4" 263 | 264 | [package.extras] 265 | docs = ["sphinx", "rst.linker", "jaraco.packaging"] 266 | 267 | [[package]] 268 | category = "dev" 269 | description = "A very fast and expressive template engine." 270 | name = "jinja2" 271 | optional = false 272 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 273 | version = "2.11.1" 274 | 275 | [package.dependencies] 276 | MarkupSafe = ">=0.23" 277 | 278 | [package.extras] 279 | i18n = ["Babel (>=0.8)"] 280 | 281 | [[package]] 282 | category = "dev" 283 | description = "JSON Matching Expressions" 284 | name = "jmespath" 285 | optional = false 286 | python-versions = "*" 287 | version = "0.9.5" 288 | 289 | [[package]] 290 | category = "dev" 291 | description = "Python LiveReload is an awesome tool for web developers" 292 | name = "livereload" 293 | optional = false 294 | python-versions = "*" 295 | version = "2.6.1" 296 | 297 | [package.dependencies] 298 | six = "*" 299 | tornado = "*" 300 | 301 | [[package]] 302 | category = "dev" 303 | description = "A Python implementation of Lunr.js" 304 | name = "lunr" 305 | optional = false 306 | python-versions = "*" 307 | version = "0.5.6" 308 | 309 | [package.dependencies] 310 | future = ">=0.16.0" 311 | six = ">=1.11.0" 312 | 313 | [package.dependencies.nltk] 314 | optional = true 315 | version = ">=3.2.5" 316 | 317 | [package.extras] 318 | languages = ["nltk (>=3.2.5)"] 319 | 320 | [[package]] 321 | category = "dev" 322 | description = "Python implementation of Markdown." 323 | name = "markdown" 324 | optional = false 325 | python-versions = ">=3.5" 326 | version = "3.2.1" 327 | 328 | [package.dependencies] 329 | setuptools = ">=36" 330 | 331 | [package.extras] 332 | testing = ["coverage", "pyyaml"] 333 | 334 | [[package]] 335 | category = "dev" 336 | description = "Safely add untrusted strings to HTML/XML markup." 337 | name = "markupsafe" 338 | optional = false 339 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 340 | version = "1.1.1" 341 | 342 | [[package]] 343 | category = "dev" 344 | description = "Project documentation with Markdown." 345 | name = "mkdocs" 346 | optional = false 347 | python-versions = ">=3.5" 348 | version = "1.1" 349 | 350 | [package.dependencies] 351 | Jinja2 = ">=2.10.1" 352 | Markdown = ">=3.2.1" 353 | PyYAML = ">=3.10" 354 | click = ">=3.3" 355 | livereload = ">=2.5.1" 356 | tornado = ">=5.0" 357 | 358 | [package.dependencies.lunr] 359 | extras = ["languages"] 360 | version = "0.5.6" 361 | 362 | [[package]] 363 | category = "dev" 364 | description = "More routines for operating on iterables, beyond itertools" 365 | marker = "python_version > \"2.7\"" 366 | name = "more-itertools" 367 | optional = false 368 | python-versions = ">=3.5" 369 | version = "8.2.0" 370 | 371 | [[package]] 372 | category = "dev" 373 | description = "Natural Language Toolkit" 374 | name = "nltk" 375 | optional = false 376 | python-versions = "*" 377 | version = "3.4.5" 378 | 379 | [package.dependencies] 380 | six = "*" 381 | 382 | [package.extras] 383 | all = ["pyparsing", "scikit-learn", "python-crfsuite", "matplotlib", "scipy", "gensim", "requests", "twython", "numpy"] 384 | corenlp = ["requests"] 385 | machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"] 386 | plot = ["matplotlib"] 387 | tgrep = ["pyparsing"] 388 | twitter = ["twython"] 389 | 390 | [[package]] 391 | category = "dev" 392 | description = "Node.js virtual environment builder" 393 | name = "nodeenv" 394 | optional = false 395 | python-versions = "*" 396 | version = "1.3.5" 397 | 398 | [[package]] 399 | category = "dev" 400 | description = "Core utilities for Python packages" 401 | name = "packaging" 402 | optional = false 403 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 404 | version = "20.3" 405 | 406 | [package.dependencies] 407 | pyparsing = ">=2.0.2" 408 | six = "*" 409 | 410 | [[package]] 411 | category = "dev" 412 | description = "Utility library for gitignore style pattern matching of file paths." 413 | name = "pathspec" 414 | optional = false 415 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 416 | version = "0.7.0" 417 | 418 | [[package]] 419 | category = "dev" 420 | description = "plugin and hook calling mechanisms for python" 421 | name = "pluggy" 422 | optional = false 423 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 424 | version = "0.13.1" 425 | 426 | [package.dependencies] 427 | [package.dependencies.importlib-metadata] 428 | python = "<3.8" 429 | version = ">=0.12" 430 | 431 | [package.extras] 432 | dev = ["pre-commit", "tox"] 433 | 434 | [[package]] 435 | category = "dev" 436 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 437 | name = "pre-commit" 438 | optional = false 439 | python-versions = ">=3.6.1" 440 | version = "2.2.0" 441 | 442 | [package.dependencies] 443 | cfgv = ">=2.0.0" 444 | identify = ">=1.0.0" 445 | nodeenv = ">=0.11.1" 446 | pyyaml = ">=5.1" 447 | toml = "*" 448 | virtualenv = ">=15.2" 449 | 450 | [package.dependencies.importlib-metadata] 451 | python = "<3.8" 452 | version = "*" 453 | 454 | [package.dependencies.importlib-resources] 455 | python = "<3.7" 456 | version = "*" 457 | 458 | [[package]] 459 | category = "dev" 460 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 461 | name = "py" 462 | optional = false 463 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 464 | version = "1.8.1" 465 | 466 | [[package]] 467 | category = "dev" 468 | description = "Python parsing module" 469 | name = "pyparsing" 470 | optional = false 471 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 472 | version = "2.4.6" 473 | 474 | [[package]] 475 | category = "dev" 476 | description = "pytest: simple powerful testing with Python" 477 | name = "pytest" 478 | optional = false 479 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 480 | version = "4.6.9" 481 | 482 | [package.dependencies] 483 | atomicwrites = ">=1.0" 484 | attrs = ">=17.4.0" 485 | packaging = "*" 486 | pluggy = ">=0.12,<1.0" 487 | py = ">=1.5.0" 488 | six = ">=1.10.0" 489 | wcwidth = "*" 490 | 491 | [package.dependencies.colorama] 492 | python = "<3.4.0 || >=3.5.0" 493 | version = "*" 494 | 495 | [package.dependencies.importlib-metadata] 496 | python = "<3.8" 497 | version = ">=0.12" 498 | 499 | [package.dependencies.more-itertools] 500 | python = ">=2.8" 501 | version = ">=4.0.0" 502 | 503 | [package.extras] 504 | testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] 505 | 506 | [[package]] 507 | category = "dev" 508 | description = "Pytest plugin for measuring coverage." 509 | name = "pytest-cov" 510 | optional = false 511 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 512 | version = "2.8.1" 513 | 514 | [package.dependencies] 515 | coverage = ">=4.4" 516 | pytest = ">=3.6" 517 | 518 | [package.extras] 519 | testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] 520 | 521 | [[package]] 522 | category = "dev" 523 | description = "run tests in isolated forked subprocesses" 524 | name = "pytest-forked" 525 | optional = false 526 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 527 | version = "1.1.3" 528 | 529 | [package.dependencies] 530 | pytest = ">=3.1.0" 531 | 532 | [[package]] 533 | category = "dev" 534 | description = "pytest xdist plugin for distributed testing and loop-on-failing modes" 535 | name = "pytest-xdist" 536 | optional = false 537 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 538 | version = "1.31.0" 539 | 540 | [package.dependencies] 541 | execnet = ">=1.1" 542 | pytest = ">=4.4.0" 543 | pytest-forked = "*" 544 | six = "*" 545 | 546 | [package.extras] 547 | testing = ["filelock"] 548 | 549 | [[package]] 550 | category = "dev" 551 | description = "Extensions to the standard Python datetime module" 552 | name = "python-dateutil" 553 | optional = false 554 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 555 | version = "2.8.1" 556 | 557 | [package.dependencies] 558 | six = ">=1.5" 559 | 560 | [[package]] 561 | category = "dev" 562 | description = "YAML parser and emitter for Python" 563 | name = "pyyaml" 564 | optional = false 565 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 566 | version = "5.3.1" 567 | 568 | [[package]] 569 | category = "dev" 570 | description = "Alternative regular expression module, to replace re." 571 | name = "regex" 572 | optional = false 573 | python-versions = "*" 574 | version = "2020.4.4" 575 | 576 | [[package]] 577 | category = "dev" 578 | description = "Python HTTP for Humans." 579 | name = "requests" 580 | optional = false 581 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 582 | version = "2.23.0" 583 | 584 | [package.dependencies] 585 | certifi = ">=2017.4.17" 586 | chardet = ">=3.0.2,<4" 587 | idna = ">=2.5,<3" 588 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 589 | 590 | [package.extras] 591 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 592 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 593 | 594 | [[package]] 595 | category = "dev" 596 | description = "An Amazon S3 Transfer Manager" 597 | name = "s3transfer" 598 | optional = false 599 | python-versions = "*" 600 | version = "0.3.3" 601 | 602 | [package.dependencies] 603 | botocore = ">=1.12.36,<2.0a.0" 604 | 605 | [[package]] 606 | category = "dev" 607 | description = "Python 2 and 3 compatibility utilities" 608 | name = "six" 609 | optional = false 610 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 611 | version = "1.14.0" 612 | 613 | [[package]] 614 | category = "dev" 615 | description = "Python Library for Tom's Obvious, Minimal Language" 616 | name = "toml" 617 | optional = false 618 | python-versions = "*" 619 | version = "0.10.0" 620 | 621 | [[package]] 622 | category = "dev" 623 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 624 | name = "tornado" 625 | optional = false 626 | python-versions = ">= 3.5" 627 | version = "6.0.4" 628 | 629 | [[package]] 630 | category = "dev" 631 | description = "a fork of Python 2 and 3 ast modules with type comment support" 632 | name = "typed-ast" 633 | optional = false 634 | python-versions = "*" 635 | version = "1.4.1" 636 | 637 | [[package]] 638 | category = "dev" 639 | description = "HTTP library with thread-safe connection pooling, file post, and more." 640 | name = "urllib3" 641 | optional = false 642 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 643 | version = "1.25.8" 644 | 645 | [package.extras] 646 | brotli = ["brotlipy (>=0.6.0)"] 647 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 648 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 649 | 650 | [[package]] 651 | category = "dev" 652 | description = "Virtual Python Environment builder" 653 | name = "virtualenv" 654 | optional = false 655 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 656 | version = "20.0.17" 657 | 658 | [package.dependencies] 659 | appdirs = ">=1.4.3,<2" 660 | distlib = ">=0.3.0,<1" 661 | filelock = ">=3.0.0,<4" 662 | six = ">=1.9.0,<2" 663 | 664 | [package.dependencies.importlib-metadata] 665 | python = "<3.8" 666 | version = ">=0.12,<2" 667 | 668 | [package.dependencies.importlib-resources] 669 | python = "<3.7" 670 | version = ">=1.0,<2" 671 | 672 | [package.extras] 673 | docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"] 674 | testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.16,<1)"] 675 | 676 | [[package]] 677 | category = "dev" 678 | description = "Measures number of Terminal column cells of wide-character codes" 679 | name = "wcwidth" 680 | optional = false 681 | python-versions = "*" 682 | version = "0.1.9" 683 | 684 | [[package]] 685 | category = "dev" 686 | description = "Backport of pathlib-compatible object wrapper for zip files" 687 | marker = "python_version < \"3.8\"" 688 | name = "zipp" 689 | optional = false 690 | python-versions = ">=3.6" 691 | version = "3.1.0" 692 | 693 | [package.extras] 694 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 695 | testing = ["jaraco.itertools", "func-timeout"] 696 | 697 | [metadata] 698 | content-hash = "3587940eabf8ce225a07203aa5486a728850f73f3a473c6787fe9438d50b2eaa" 699 | python-versions = "^3.6.1" 700 | 701 | [metadata.files] 702 | apipkg = [ 703 | {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, 704 | {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, 705 | ] 706 | appdirs = [ 707 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 708 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 709 | ] 710 | atomicwrites = [ 711 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 712 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 713 | ] 714 | attrs = [ 715 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 716 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 717 | ] 718 | black = [ 719 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 720 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 721 | ] 722 | boto3 = [ 723 | {file = "boto3-1.12.36-py2.py3-none-any.whl", hash = "sha256:57397f9ad3e9afc17e6100a38c6e631b6545aabc7f8c38b86ff2c6f5931d0ebf"}, 724 | {file = "boto3-1.12.36.tar.gz", hash = "sha256:911994ef46595e8ab9f08eee6b666caea050937b96d54394292e958330cd7ad5"}, 725 | ] 726 | botocore = [ 727 | {file = "botocore-1.15.36-py2.py3-none-any.whl", hash = "sha256:d2233e19b6a60792185b15fe4cd8f92c5579298e5079daf17f66f6e639585e3a"}, 728 | {file = "botocore-1.15.36.tar.gz", hash = "sha256:5fc5e8629b5375d591a47d05baaff6ccdf5c3b617aad1e14286be458092c9e53"}, 729 | ] 730 | certifi = [ 731 | {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, 732 | {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, 733 | ] 734 | cfgv = [ 735 | {file = "cfgv-3.1.0-py2.py3-none-any.whl", hash = "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53"}, 736 | {file = "cfgv-3.1.0.tar.gz", hash = "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"}, 737 | ] 738 | chardet = [ 739 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 740 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 741 | ] 742 | click = [ 743 | {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, 744 | {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, 745 | ] 746 | colorama = [ 747 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 748 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 749 | ] 750 | coverage = [ 751 | {file = "coverage-5.0.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307"}, 752 | {file = "coverage-5.0.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8"}, 753 | {file = "coverage-5.0.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31"}, 754 | {file = "coverage-5.0.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441"}, 755 | {file = "coverage-5.0.4-cp27-cp27m-win32.whl", hash = "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac"}, 756 | {file = "coverage-5.0.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435"}, 757 | {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037"}, 758 | {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a"}, 759 | {file = "coverage-5.0.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5"}, 760 | {file = "coverage-5.0.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30"}, 761 | {file = "coverage-5.0.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7"}, 762 | {file = "coverage-5.0.4-cp35-cp35m-win32.whl", hash = "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de"}, 763 | {file = "coverage-5.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1"}, 764 | {file = "coverage-5.0.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"}, 765 | {file = "coverage-5.0.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0"}, 766 | {file = "coverage-5.0.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd"}, 767 | {file = "coverage-5.0.4-cp36-cp36m-win32.whl", hash = "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0"}, 768 | {file = "coverage-5.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b"}, 769 | {file = "coverage-5.0.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78"}, 770 | {file = "coverage-5.0.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6"}, 771 | {file = "coverage-5.0.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014"}, 772 | {file = "coverage-5.0.4-cp37-cp37m-win32.whl", hash = "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732"}, 773 | {file = "coverage-5.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006"}, 774 | {file = "coverage-5.0.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2"}, 775 | {file = "coverage-5.0.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe"}, 776 | {file = "coverage-5.0.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9"}, 777 | {file = "coverage-5.0.4-cp38-cp38-win32.whl", hash = "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1"}, 778 | {file = "coverage-5.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0"}, 779 | {file = "coverage-5.0.4-cp39-cp39-win32.whl", hash = "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7"}, 780 | {file = "coverage-5.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892"}, 781 | {file = "coverage-5.0.4.tar.gz", hash = "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823"}, 782 | ] 783 | coveralls = [ 784 | {file = "coveralls-1.11.1-py2.py3-none-any.whl", hash = "sha256:4b6bfc2a2a77b890f556bc631e35ba1ac21193c356393b66c84465c06218e135"}, 785 | {file = "coveralls-1.11.1.tar.gz", hash = "sha256:67188c7ec630c5f708c31552f2bcdac4580e172219897c4136504f14b823132f"}, 786 | ] 787 | distlib = [ 788 | {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, 789 | ] 790 | docopt = [ 791 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 792 | ] 793 | docutils = [ 794 | {file = "docutils-0.15.2-py2-none-any.whl", hash = "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827"}, 795 | {file = "docutils-0.15.2-py3-none-any.whl", hash = "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0"}, 796 | {file = "docutils-0.15.2.tar.gz", hash = "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"}, 797 | ] 798 | execnet = [ 799 | {file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"}, 800 | {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, 801 | ] 802 | filelock = [ 803 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 804 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 805 | ] 806 | future = [ 807 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 808 | ] 809 | identify = [ 810 | {file = "identify-1.4.14-py2.py3-none-any.whl", hash = "sha256:2bb8760d97d8df4408f4e805883dad26a2d076f04be92a10a3e43f09c6060742"}, 811 | {file = "identify-1.4.14.tar.gz", hash = "sha256:faffea0fd8ec86bb146ac538ac350ed0c73908326426d387eded0bcc9d077522"}, 812 | ] 813 | idna = [ 814 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 815 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 816 | ] 817 | importlib-metadata = [ 818 | {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, 819 | {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, 820 | ] 821 | importlib-resources = [ 822 | {file = "importlib_resources-1.4.0-py2.py3-none-any.whl", hash = "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"}, 823 | {file = "importlib_resources-1.4.0.tar.gz", hash = "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2"}, 824 | ] 825 | jinja2 = [ 826 | {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"}, 827 | {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"}, 828 | ] 829 | jmespath = [ 830 | {file = "jmespath-0.9.5-py2.py3-none-any.whl", hash = "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec"}, 831 | {file = "jmespath-0.9.5.tar.gz", hash = "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"}, 832 | ] 833 | livereload = [ 834 | {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, 835 | {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, 836 | ] 837 | lunr = [ 838 | {file = "lunr-0.5.6-py2.py3-none-any.whl", hash = "sha256:1208622930c915a07e6f8e8640474357826bad48534c0f57969b6fca9bffc88e"}, 839 | {file = "lunr-0.5.6.tar.gz", hash = "sha256:7be69d7186f65784a4f2adf81e5c58efd6a9921aa95966babcb1f2f2ada75c20"}, 840 | ] 841 | markdown = [ 842 | {file = "Markdown-3.2.1-py2.py3-none-any.whl", hash = "sha256:e4795399163109457d4c5af2183fbe6b60326c17cfdf25ce6e7474c6624f725d"}, 843 | {file = "Markdown-3.2.1.tar.gz", hash = "sha256:90fee683eeabe1a92e149f7ba74e5ccdc81cd397bd6c516d93a8da0ef90b6902"}, 844 | ] 845 | markupsafe = [ 846 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 847 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 848 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 849 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 850 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 851 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 852 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 853 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 854 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 855 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 856 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 857 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 858 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 859 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 860 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 861 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 862 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 863 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 864 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 865 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 866 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 867 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 868 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 869 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 870 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 871 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 872 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 873 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 874 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 875 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 876 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 877 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 878 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 879 | ] 880 | mkdocs = [ 881 | {file = "mkdocs-1.1-py2.py3-none-any.whl", hash = "sha256:1e385a70aea8a9dedb731aea4fd5f3704b2074801c4f96f06b2920999babda8a"}, 882 | {file = "mkdocs-1.1.tar.gz", hash = "sha256:9243291392f59e20b655e4e46210233453faf97787c2cf72176510e868143174"}, 883 | ] 884 | more-itertools = [ 885 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 886 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 887 | ] 888 | nltk = [ 889 | {file = "nltk-3.4.5.win32.exe", hash = "sha256:a08bdb4b8a1c13de16743068d9eb61c8c71c2e5d642e8e08205c528035843f82"}, 890 | {file = "nltk-3.4.5.zip", hash = "sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94"}, 891 | ] 892 | nodeenv = [ 893 | {file = "nodeenv-1.3.5-py2.py3-none-any.whl", hash = "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"}, 894 | ] 895 | packaging = [ 896 | {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, 897 | {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, 898 | ] 899 | pathspec = [ 900 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 901 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 902 | ] 903 | pluggy = [ 904 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 905 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 906 | ] 907 | pre-commit = [ 908 | {file = "pre_commit-2.2.0-py2.py3-none-any.whl", hash = "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522"}, 909 | {file = "pre_commit-2.2.0.tar.gz", hash = "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1"}, 910 | ] 911 | py = [ 912 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 913 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 914 | ] 915 | pyparsing = [ 916 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 917 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 918 | ] 919 | pytest = [ 920 | {file = "pytest-4.6.9-py2.py3-none-any.whl", hash = "sha256:c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"}, 921 | {file = "pytest-4.6.9.tar.gz", hash = "sha256:19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339"}, 922 | ] 923 | pytest-cov = [ 924 | {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, 925 | {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, 926 | ] 927 | pytest-forked = [ 928 | {file = "pytest-forked-1.1.3.tar.gz", hash = "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100"}, 929 | {file = "pytest_forked-1.1.3-py2.py3-none-any.whl", hash = "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87"}, 930 | ] 931 | pytest-xdist = [ 932 | {file = "pytest-xdist-1.31.0.tar.gz", hash = "sha256:7dc0d027d258cd0defc618fb97055fbd1002735ca7a6d17037018cf870e24011"}, 933 | {file = "pytest_xdist-1.31.0-py2.py3-none-any.whl", hash = "sha256:0f46020d3d9619e6d17a65b5b989c1ebbb58fc7b1da8fb126d70f4bac4dfeed1"}, 934 | ] 935 | python-dateutil = [ 936 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 937 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 938 | ] 939 | pyyaml = [ 940 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 941 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 942 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 943 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 944 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 945 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 946 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 947 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 948 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 949 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 950 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 951 | ] 952 | regex = [ 953 | {file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"}, 954 | {file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"}, 955 | {file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"}, 956 | {file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"}, 957 | {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"}, 958 | {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"}, 959 | {file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"}, 960 | {file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"}, 961 | {file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"}, 962 | {file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"}, 963 | {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"}, 964 | {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"}, 965 | {file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"}, 966 | {file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"}, 967 | {file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"}, 968 | {file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"}, 969 | {file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"}, 970 | {file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"}, 971 | {file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"}, 972 | {file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"}, 973 | {file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"}, 974 | ] 975 | requests = [ 976 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 977 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 978 | ] 979 | s3transfer = [ 980 | {file = "s3transfer-0.3.3-py2.py3-none-any.whl", hash = "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13"}, 981 | {file = "s3transfer-0.3.3.tar.gz", hash = "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"}, 982 | ] 983 | six = [ 984 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 985 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 986 | ] 987 | toml = [ 988 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 989 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 990 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 991 | ] 992 | tornado = [ 993 | {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, 994 | {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, 995 | {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, 996 | {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, 997 | {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, 998 | {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, 999 | {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, 1000 | {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, 1001 | {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, 1002 | ] 1003 | typed-ast = [ 1004 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 1005 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 1006 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 1007 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 1008 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 1009 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 1010 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 1011 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 1012 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 1013 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 1014 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 1015 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 1016 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 1017 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 1018 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 1019 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 1020 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 1021 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 1022 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 1023 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 1024 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 1025 | ] 1026 | urllib3 = [ 1027 | {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, 1028 | {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, 1029 | ] 1030 | virtualenv = [ 1031 | {file = "virtualenv-20.0.17-py2.py3-none-any.whl", hash = "sha256:00cfe8605fb97f5a59d52baab78e6070e72c12ca64f51151695407cc0eb8a431"}, 1032 | {file = "virtualenv-20.0.17.tar.gz", hash = "sha256:c8364ec469084046c779c9a11ae6340094e8a0bf1d844330fc55c1cefe67c172"}, 1033 | ] 1034 | wcwidth = [ 1035 | {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, 1036 | {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, 1037 | ] 1038 | zipp = [ 1039 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 1040 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 1041 | ] 1042 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "lambda-cache" 3 | version = "0.8.1" 4 | description = "Python utility for simple caching in AWS Lambda functions" 5 | authors = ["keithrozario "] 6 | documentation = "https://lambda-cache.readthedocs.io/en/latest/" 7 | repository = "https://github.com/keithrozario/lambda-cache" 8 | homepage = "https://github.com/keithrozario/lambda-cache" 9 | classifiers=[ 10 | "Development Status :: 4 - Beta", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Natural Language :: English", 14 | "Programming Language :: Python :: 3.6", 15 | "Programming Language :: Python :: 3.7", 16 | "Programming Language :: Python :: 3.8", 17 | ] 18 | readme = "README.md" 19 | license = "MIT" 20 | keywords = ["aws", "aws_parameter_store", "aws_lambda", "cache"] 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.6.1" 24 | 25 | [tool.poetry.dev-dependencies] 26 | pytest = "^4.6" 27 | black = "^19.10b0" 28 | boto3 = "^1.12.31" 29 | PyYaml = "^5.3.1" 30 | pip = "^20.0.2" 31 | pytest-cov = "^2.8.1" 32 | coverage = "^5.0.4" 33 | coveralls = "^1.11.1" 34 | toml = "^0.10.0" 35 | mkdocs = "^1.1" 36 | pytest-xdist = "^1.31.0" 37 | pre-commit = "^2.2.0" 38 | 39 | [tool.poetry.urls] 40 | "Bug Tracker" = "https://github.com/keithrozario/lambda_cache/issues" 41 | "Docs" = "https://lambda-cache.readthedocs.io" 42 | 43 | [build-system] 44 | requires = ["poetry>=0.12"] 45 | build-backend = "poetry.masonry.api" 46 | 47 | -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = lambda_cache -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithrozario/lambda-cache/13b6c8e4f04697ae52a170fa5ec0f44e8b29aee2/tests/__init__.py -------------------------------------------------------------------------------- /tests/acceptance_tests/_test_ssm.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | 4 | from lambda_cache import ssm 5 | from datetime import datetime 6 | 7 | # this file is packaged in the lambda using serverless.yml 8 | from variables_data import * 9 | 10 | 11 | def generic_return(message): 12 | body = { 13 | "message": message 14 | } 15 | response = { 16 | "statusCode": 200, 17 | "body": json.dumps(body) 18 | } 19 | return response 20 | 21 | @ssm.cache(parameter=[ssm_parameter, ssm_parameter_2, string_list_parameter, secure_parameter], entry_name=default_entry_name, max_age_in_seconds=10) 22 | def multi_parameter_2(event, context): 23 | 24 | cache = getattr(context, default_entry_name) 25 | 26 | message = {"param_1": cache.get(ssm_parameter_default_name), 27 | "param_2": cache.get(ssm_parameter_2_default_name), 28 | "param_3": cache.get(string_list_default_name), 29 | "param_4": cache.get(secure_parameter_default_name)} 30 | 31 | client = boto3.client('ssm') 32 | response = client.put_parameter( 33 | Name=ssm_parameter, 34 | Value=datetime.now().isoformat(), 35 | Type='String', 36 | Overwrite=True) 37 | 38 | return generic_return(message) -------------------------------------------------------------------------------- /tests/acceptance_tests/serverless.yml: -------------------------------------------------------------------------------- 1 | service: aws-lambda-cache-acceptance 2 | 3 | custom: 4 | parameter_name: /lambda_cache/something 5 | parameter_name_2 : /lambda_cache/test/something_else 6 | secure_string: /lambda_cache/test/secure/somethingsecure 7 | string_list: /lambda/cache/test/somelist 8 | s3_bucket_name: /lambda-cache/s3/bucket_name 9 | 10 | provider: 11 | name: aws 12 | runtime: python3.6 13 | stage: test 14 | region: ${env:AWS_DEFAULT_REGION, 'ap-southeast-1'} 15 | environment: 16 | S3_BUCKET: ${ssm:/lambda-cache/s3/bucket_name} 17 | iamRoleStatements: 18 | - Effect: Allow 19 | Action: 20 | - ssm:GetParameter 21 | - ssm:GetParameters 22 | - ssm:PutParameter 23 | Resource: 24 | - Fn::Join: 25 | - ":" 26 | - - "arn:aws:ssm" 27 | - Ref: AWS::Region 28 | - Ref: AWS::AccountId 29 | - "parameter${self:custom.parameter_name}" 30 | - Fn::Join: 31 | - ":" 32 | - - "arn:aws:ssm" 33 | - Ref: AWS::Region 34 | - Ref: AWS::AccountId 35 | - "parameter${self:custom.parameter_name_2}" 36 | - Fn::Join: 37 | - ":" 38 | - - "arn:aws:ssm" 39 | - Ref: AWS::Region 40 | - Ref: AWS::AccountId 41 | - "parameter${self:custom.secure_string}" 42 | - Fn::Join: 43 | - ":" 44 | - - "arn:aws:ssm" 45 | - Ref: AWS::Region 46 | - Ref: AWS::AccountId 47 | - "parameter${self:custom.string_list}" 48 | - Fn::Join: 49 | - ":" 50 | - - "arn:aws:ssm" 51 | - Ref: AWS::Region 52 | - Ref: AWS::AccountId 53 | - "parameter${self:custom.s3_bucket_name}" 54 | - Effect: Allow 55 | Action: 56 | - kms:Decrypt 57 | Resource: 58 | Fn::Join: 59 | - ":" 60 | - - "arn:aws:kms" 61 | - Ref: AWS::Region 62 | - Ref: AWS::AccountId 63 | - "alias/aws/ssm" # Default AWS Managed CMK Key 64 | - Effect: Allow 65 | Action: 66 | - secretsmanager:GetSecretValue 67 | Resource: 68 | Fn::Join: 69 | - ":" 70 | - - "arn:aws:secretsmanager" 71 | - Ref: AWS::Region 72 | - Ref: AWS::AccountId 73 | - "secret" 74 | - "*" 75 | - Effect: Allow 76 | Action: 77 | - s3:GetObject 78 | Resource: "*" 79 | 80 | 81 | package: 82 | include: 83 | - ../variables_data.py 84 | exclude: 85 | - .pytest_cache/** 86 | - __pycache__/** 87 | - node_modules/** 88 | - package.json 89 | - package-lock.json 90 | - tests/** 91 | 92 | functions: 93 | acceptance_test: 94 | handler: _test_ssm.multi_parameter_2 95 | layers: 96 | - arn:aws:lambda:ap-southeast-1:908645878701:layer:pylayers-python38-defaultlambda-cache:1 97 | 98 | -------------------------------------------------------------------------------- /tests/context_object.py: -------------------------------------------------------------------------------- 1 | 2 | class LambdaContext(object): 3 | """ Dummy Lambda Context Object""" 4 | 5 | def __init__(self, invokeid="1234", context_objs=None, client_context=None, invoked_function_arn=None): 6 | dummy_value = "_" 7 | 8 | self.aws_request_id = invokeid 9 | self.log_group_name = dummy_value 10 | self.log_stream_name = dummy_value 11 | self.function_name = dummy_value 12 | self.memory_limit_in_mb = dummy_value 13 | self.function_version = dummy_value 14 | self.invoked_function_arn = invoked_function_arn 15 | 16 | # self.client_context = make_obj_from_dict(ClientContext, client_context) 17 | # if self.client_context is not None: 18 | # self.client_context.client = make_obj_from_dict(Client, self.client_context.client) 19 | # self.identity = make_obj_from_dict(CognitoIdentity, context_objs) 20 | 21 | def get_remaining_time_in_millis(self): 22 | return "No time left" 23 | 24 | def log(self, msg): 25 | print ("logging out") -------------------------------------------------------------------------------- /tests/helper_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import tempfile 3 | 4 | import boto3 5 | 6 | from botocore.exceptions import ClientError 7 | from tests.variables_data import s3_bucket_ssm_param, s3_key 8 | 9 | 10 | def update_parameter(parameter, value, param_type='String'): 11 | """ 12 | Updates parameter to new value 13 | """ 14 | ssm_client = boto3.client('ssm') 15 | response = ssm_client.put_parameter( 16 | Name=parameter, 17 | Value=value, 18 | Type=param_type, 19 | Overwrite=True, 20 | Tier='Standard' 21 | ) 22 | return 23 | 24 | def delete_parameters(parameters: list): 25 | ssm_client = boto3.client('ssm') 26 | response = ssm_client.delete_parameters( 27 | Names=parameters 28 | ) 29 | return 30 | 31 | def update_secret(name, value, secret_type='String'): 32 | """ 33 | Updates secret to new value 34 | """ 35 | 36 | secrets_client = boto3.client('secretsmanager') 37 | 38 | if secret_type == 'String': 39 | try: 40 | response = secrets_client.put_secret_value( 41 | SecretId=name, 42 | SecretString=value 43 | ) 44 | except ClientError as e: 45 | if e.response['Error']['Code'] == "ResourceNotFoundException": 46 | response = secrets_client.create_secret( 47 | Name=name, 48 | SecretString=value 49 | ) 50 | else: 51 | try: 52 | response = secrets_client.put_secret_value( 53 | SecretId=name, 54 | SecretBinary=value 55 | ) 56 | except ClientError as e: 57 | if e.response['Error']['Code'] == "ResourceNotFoundException": 58 | response = secrets_client.create_secret( 59 | Name=name, 60 | SecretBinary=value 61 | ) 62 | return 63 | 64 | def get_s3_bucket_name(): 65 | """ 66 | Gets the name of the s3 bucket used for test 67 | """ 68 | 69 | ssm_client = boto3.client('ssm') 70 | response = ssm_client.get_parameter( 71 | Name=s3_bucket_ssm_param 72 | ) 73 | 74 | bucket_name = response['Parameter']['Value'] 75 | return bucket_name 76 | 77 | def upload_object(file_name, bucket_name, key): 78 | """ 79 | upload file to s3 80 | """ 81 | 82 | s3 = boto3.resource('s3') 83 | s3.meta.client.upload_file(file_name, bucket_name, key) 84 | 85 | return 86 | 87 | def check_status_file(bucket_name, key): 88 | """ 89 | Get content of file 90 | """ 91 | 92 | s3 = boto3.client('s3') 93 | with tempfile.TemporaryFile() as data: 94 | s3.download_fileobj(bucket_name, s3_key, data) 95 | data.seek(0) 96 | status = json.loads(data.read().decode('utf-8'))['status'] 97 | 98 | return status -------------------------------------------------------------------------------- /tests/integration_tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | *.pyc 4 | env/ 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | 20 | # Serverless directories 21 | .serverless 22 | -------------------------------------------------------------------------------- /tests/integration_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithrozario/lambda-cache/13b6c8e4f04697ae52a170fa5ec0f44e8b29aee2/tests/integration_tests/__init__.py -------------------------------------------------------------------------------- /tests/integration_tests/_test_s3.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import os 4 | 5 | from lambda_cache import s3 6 | from variables_data import s3_bucket_ssm_param, s3_key 7 | 8 | def generic_return(message): 9 | body = { 10 | "message": message 11 | } 12 | response = { 13 | "statusCode": 200, 14 | "body": json.dumps(body) 15 | } 16 | return response 17 | 18 | 19 | @s3.cache(s3Uri=f"s3://{os.environ['S3_BUCKET']}/{s3_key}", entry_name='temp_key.json', max_age_in_seconds=5, check_before_download=False) 20 | def s3_download(event, context): 21 | with open("/tmp/temp_key.json") as file_data: 22 | status = json.loads(file_data.read())['status'] 23 | 24 | return generic_return(status) -------------------------------------------------------------------------------- /tests/integration_tests/_test_secrets.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lambda_cache import secrets_manager 3 | 4 | # this file is packaged in the lambda using serverless.yml 5 | from variables_data import * 6 | 7 | 8 | def generic_return(message): 9 | body = { 10 | "message": message 11 | } 12 | response = { 13 | "statusCode": 200, 14 | "body": json.dumps(body) 15 | } 16 | return response 17 | 18 | 19 | @secrets_manager.cache(name=secret_name_string, max_age_in_seconds=5) 20 | def secret_string(event, context): 21 | message = getattr(context, secret_name_string.split('/')[-1]) 22 | return generic_return(message) 23 | 24 | @secrets_manager.cache(name=secret_name_binary, max_age_in_seconds=5) 25 | def secret_binary(event, context): 26 | message = getattr(context, secret_name_binary.split('/')[-1]) 27 | return generic_return(message.decode('utf-8')) 28 | 29 | @secrets_manager.cache(name=secret_name_string) 30 | def secret_string_default(event, context): 31 | message = getattr(context, secret_name_string.split('/')[-1]) 32 | return generic_return(message) -------------------------------------------------------------------------------- /tests/integration_tests/_test_ssm.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lambda_cache import ssm 3 | 4 | # this file is packaged in the lambda using serverless.yml 5 | from variables_data import * 6 | 7 | 8 | def generic_return(message): 9 | body = { 10 | "message": message 11 | } 12 | response = { 13 | "statusCode": 200, 14 | "body": json.dumps(body) 15 | } 16 | return response 17 | 18 | 19 | @ssm.cache(ssm_parameter, max_age_in_seconds=5) 20 | def single_parameter(event, context): 21 | message = getattr(context, ssm_parameter.split('/')[-1]) 22 | return generic_return(message) 23 | 24 | @ssm.cache(ssm_parameter_2, max_age_in_seconds=5, entry_name='default_param') 25 | def rename_param(event, context): 26 | message = getattr(context, 'default_param') 27 | return generic_return(message) 28 | 29 | @ssm.cache(secure_parameter) 30 | def secure_string(event, context): 31 | message = getattr(context, secure_parameter.split('/')[-1]) 32 | return generic_return(message) 33 | 34 | @ssm.cache(string_list_parameter, max_age_in_seconds=5) 35 | def string_list(event, context): 36 | message = getattr(context, string_list_parameter.split('/')[-1]) 37 | return generic_return(message) 38 | 39 | @ssm.cache(secure_parameter) 40 | @ssm.cache(string_list_parameter, max_age_in_seconds=10) 41 | @ssm.cache(ssm_parameter, max_age_in_seconds=10) 42 | @ssm.cache(ssm_parameter_2, max_age_in_seconds=10) 43 | def multi_parameter(event, context): 44 | message = {"param_1": getattr(context,ssm_parameter_default_name), 45 | "param_2": getattr(context,ssm_parameter_2_default_name), 46 | "param_3": getattr(context,string_list_default_name), 47 | "param_4": getattr(context,secure_parameter_default_name)} 48 | return generic_return(message) 49 | 50 | @ssm.cache(parameter=[ssm_parameter, ssm_parameter_2, string_list_parameter, secure_parameter], entry_name=default_entry_name, max_age_in_seconds=10) 51 | def multi_parameter_2(event, context): 52 | 53 | cache = getattr(context, default_entry_name) 54 | 55 | message = {"param_1": cache.get(ssm_parameter_default_name), 56 | "param_2": cache.get(ssm_parameter_2_default_name), 57 | "param_3": cache.get(string_list_default_name), 58 | "param_4": cache.get(secure_parameter_default_name)} 59 | return generic_return(message) 60 | 61 | 62 | def assign_parameter(event, context): 63 | message = {"param_1": ssm.get_entry(parameter=ssm_parameter, max_age_in_seconds=20), 64 | "param_2": ssm.get_entry(parameter=ssm_parameter_2, max_age_in_seconds=30), 65 | "param_3": ssm.get_entry(parameter=string_list_parameter, max_age_in_seconds=40), 66 | "param_4": ssm.get_entry(parameter=secure_parameter)} 67 | return generic_return(message) -------------------------------------------------------------------------------- /tests/integration_tests/helper_functions.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import yaml 3 | import json 4 | 5 | 6 | def get_message_from_lambda(function_name): 7 | 8 | client = boto3.client('lambda') 9 | 10 | response = client.invoke( 11 | FunctionName=function_name, 12 | InvocationType='RequestResponse', 13 | LogType='None' 14 | ) 15 | 16 | result = json.loads((response.get('Payload').read()).decode('utf-8')) 17 | return json.loads(result['body'])['message'] 18 | 19 | 20 | def get_serverless_config(): 21 | with open('serverless.yml', 'r') as config_file: 22 | config = yaml.load(config_file.read(), Loader=yaml.FullLoader) 23 | 24 | return config -------------------------------------------------------------------------------- /tests/integration_tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration_tests/serverless.yml: -------------------------------------------------------------------------------- 1 | service: aws-lambda-cache 2 | 3 | custom: 4 | parameter_name: /lambda_cache/something 5 | parameter_name_2 : /lambda_cache/test/something_else 6 | secure_string: /lambda_cache/test/secure/somethingsecure 7 | string_list: /lambda/cache/test/somelist 8 | s3_bucket_name: /lambda-cache/s3/bucket_name 9 | 10 | provider: 11 | name: aws 12 | runtime: python3.6 13 | stage: test 14 | region: ${env:AWS_DEFAULT_REGION, 'ap-southeast-1'} 15 | environment: 16 | S3_BUCKET: ${ssm:/lambda-cache/s3/bucket_name} 17 | iamRoleStatements: 18 | - Effect: Allow 19 | Action: 20 | - ssm:GetParameter 21 | - ssm:GetParameters 22 | Resource: 23 | - Fn::Join: 24 | - ":" 25 | - - "arn:aws:ssm" 26 | - Ref: AWS::Region 27 | - Ref: AWS::AccountId 28 | - "parameter${self:custom.parameter_name}" 29 | - Fn::Join: 30 | - ":" 31 | - - "arn:aws:ssm" 32 | - Ref: AWS::Region 33 | - Ref: AWS::AccountId 34 | - "parameter${self:custom.parameter_name_2}" 35 | - Fn::Join: 36 | - ":" 37 | - - "arn:aws:ssm" 38 | - Ref: AWS::Region 39 | - Ref: AWS::AccountId 40 | - "parameter${self:custom.secure_string}" 41 | - Fn::Join: 42 | - ":" 43 | - - "arn:aws:ssm" 44 | - Ref: AWS::Region 45 | - Ref: AWS::AccountId 46 | - "parameter${self:custom.string_list}" 47 | - Fn::Join: 48 | - ":" 49 | - - "arn:aws:ssm" 50 | - Ref: AWS::Region 51 | - Ref: AWS::AccountId 52 | - "parameter${self:custom.s3_bucket_name}" 53 | - Effect: Allow 54 | Action: 55 | - kms:Decrypt 56 | Resource: 57 | Fn::Join: 58 | - ":" 59 | - - "arn:aws:kms" 60 | - Ref: AWS::Region 61 | - Ref: AWS::AccountId 62 | - "alias/aws/ssm" # Default AWS Managed CMK Key 63 | - Effect: Allow 64 | Action: 65 | - secretsmanager:GetSecretValue 66 | Resource: 67 | Fn::Join: 68 | - ":" 69 | - - "arn:aws:secretsmanager" 70 | - Ref: AWS::Region 71 | - Ref: AWS::AccountId 72 | - "secret" 73 | - "*" 74 | - Effect: Allow 75 | Action: 76 | - s3:GetObject 77 | Resource: "*" 78 | 79 | 80 | package: 81 | include: 82 | - ../../lambda_cache/** 83 | - ../variables_data.py 84 | exclude: 85 | - ../../lambda_cache/__pycache__/** 86 | - .pytest_cache/** 87 | - __pycache__/** 88 | - node_modules/** 89 | - package.json 90 | - package-lock.json 91 | - tests/** 92 | 93 | functions: 94 | single_handler: 95 | handler: _test_ssm.single_parameter 96 | default_param: 97 | handler: _test_ssm.rename_param 98 | secure_string: 99 | handler: _test_ssm.secure_string 100 | string_list: 101 | handler: _test_ssm.string_list 102 | multi_handler: 103 | handler: _test_ssm.multi_parameter 104 | multi_handler_2: 105 | handler: _test_ssm.multi_parameter_2 106 | assign_parameter: 107 | handler: _test_ssm.assign_parameter 108 | secret_string: 109 | handler: _test_secrets.secret_string 110 | secret_binary: 111 | handler: _test_secrets.secret_binary 112 | secret_string_default: 113 | handler: _test_secrets.secret_string_default 114 | s3_download: 115 | handler: _test_s3.s3_download -------------------------------------------------------------------------------- /tests/integration_tests/test_deployed_lambda.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import string 4 | 5 | from tests.variables_data import * 6 | from tests.helper_functions import update_parameter, delete_parameters 7 | 8 | from tests.integration_tests.helper_functions import get_message_from_lambda, get_serverless_config 9 | 10 | def generic_test(function_name, parameter_name, parameter_value, sleep_time=60, param_type='String'): 11 | 12 | dummy_value_updated = ''.join(random.choices(string.ascii_lowercase, k = 25)) 13 | dummy_value_returned = dummy_value_updated 14 | revert_parameter_value = parameter_value 15 | if param_type == 'StringList': 16 | dummy_value_updated = ','.join(random.choices(string.ascii_lowercase, k = 5)) 17 | dummy_value_returned = dummy_value_updated.split(',') 18 | revert_parameter_value = ','.join(parameter_value) 19 | 20 | value = get_message_from_lambda(function_name) 21 | assert value == parameter_value 22 | 23 | update_parameter(parameter_name, dummy_value_updated, param_type) 24 | value = get_message_from_lambda(function_name) 25 | assert value == parameter_value 26 | 27 | time.sleep(sleep_time) 28 | value = get_message_from_lambda(function_name) 29 | assert value == dummy_value_returned 30 | 31 | update_parameter(parameter_name, revert_parameter_value, param_type) 32 | value = get_message_from_lambda(function_name) 33 | assert value == dummy_value_returned 34 | 35 | time.sleep(sleep_time) 36 | value = get_message_from_lambda(function_name) 37 | assert value == parameter_value 38 | 39 | 40 | def test_initialize(): 41 | delete_parameters([ssm_parameter, ssm_parameter_2, secure_parameter, string_list_parameter]) 42 | update_parameter(ssm_parameter, ssm_parameter_value) 43 | update_parameter(ssm_parameter_2, ssm_parameter_2_value) 44 | update_parameter(secure_parameter, secure_parameter_value, param_type='SecureString') 45 | update_parameter(string_list_parameter,string_list_value, param_type='StringList') 46 | 47 | 48 | def test_lambda_single_hander(): 49 | function_name = f"{service}-{stage}-single_handler" 50 | generic_test(function_name, ssm_parameter, ssm_parameter_value, 5) 51 | 52 | def test_default_param(): 53 | function_name = f"{service}-{stage}-default_param" 54 | generic_test(function_name, ssm_parameter_2, ssm_parameter_2_value, 5) 55 | 56 | def test_secure_string(): 57 | function_name = f"{service}-{stage}-secure_string" 58 | generic_test(function_name, secure_parameter, secure_parameter_value, param_type='SecureString') 59 | 60 | def test_string_list(): 61 | function_name = f"{service}-{stage}-string_list" 62 | generic_test(function_name, string_list_parameter, string_list_value.split(','), 5, param_type='StringList') 63 | 64 | 65 | def test_lambda_assign_parameter(): 66 | test_initialize() 67 | function_name = f"{service}-{stage}-assign_parameter" 68 | return_value = {"param_1": ssm_parameter_value, 69 | "param_2": ssm_parameter_2_value, 70 | "param_3": string_list_value.split(','), 71 | "param_4": secure_parameter_value} 72 | 73 | new_value = ''.join(random.choices(string.ascii_lowercase, k = 8)) 74 | 75 | value = get_message_from_lambda(function_name) 76 | assert value == return_value 77 | 78 | update_parameter(ssm_parameter, new_value) 79 | update_parameter(ssm_parameter_2, new_value) 80 | update_parameter(secure_parameter, new_value) 81 | value = get_message_from_lambda(function_name) 82 | assert value == return_value 83 | 84 | time.sleep(20) # after 20 seconds 85 | return_value['param_1'] = new_value 86 | value = get_message_from_lambda(function_name) 87 | assert value == return_value 88 | 89 | time.sleep(10) # after 30 seconds 90 | return_value['param_2'] = new_value 91 | value = get_message_from_lambda(function_name) 92 | assert value == return_value 93 | 94 | time.sleep(30) # after 60 seconds 95 | return_value['param_4'] = new_value 96 | value = get_message_from_lambda(function_name) 97 | assert value == return_value 98 | 99 | 100 | # Config Variables 101 | config = get_serverless_config() 102 | service = config['service'] 103 | stage = config['provider']['stage'] -------------------------------------------------------------------------------- /tests/integration_tests/test_multi_handler.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import time 4 | 5 | from tests.integration_tests.test_deployed_lambda import test_initialize 6 | from tests.variables_data import * 7 | from tests.helper_functions import update_parameter, delete_parameters 8 | from tests.integration_tests.helper_functions import get_serverless_config, get_message_from_lambda 9 | 10 | def test_lambda_multi_handler(): 11 | function_name = f"{service}-{stage}-multi_handler" 12 | return_value = {"param_1": ssm_parameter_value, 13 | "param_2": ssm_parameter_2_value, 14 | "param_3": string_list_value.split(','), 15 | "param_4": secure_parameter_value} 16 | 17 | new_value = ''.join(random.choices(string.ascii_lowercase, k = 8)) 18 | 19 | value = get_message_from_lambda(function_name) 20 | assert value == return_value 21 | 22 | update_parameter(ssm_parameter, new_value) 23 | update_parameter(ssm_parameter_2, new_value) 24 | value = get_message_from_lambda(function_name) 25 | assert value == return_value 26 | 27 | time.sleep(10) 28 | value = get_message_from_lambda(function_name) 29 | return_value["param_1"] = new_value 30 | return_value["param_2"] = new_value 31 | assert value == return_value 32 | 33 | # Config Variables 34 | config = get_serverless_config() 35 | service = config['service'] 36 | stage = config['provider']['stage'] -------------------------------------------------------------------------------- /tests/integration_tests/test_multi_handler_2.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import time 4 | 5 | from tests.integration_tests.test_deployed_lambda import test_initialize 6 | from tests.variables_data import * 7 | from tests.helper_functions import update_parameter, delete_parameters 8 | from tests.integration_tests.helper_functions import get_serverless_config, get_message_from_lambda 9 | 10 | def test_lambda_multi_handler_2(): 11 | function_name = f"{service}-{stage}-multi_handler_2" 12 | return_value = {"param_1": ssm_parameter_value, 13 | "param_2": ssm_parameter_2_value, 14 | "param_3": string_list_value.split(','), 15 | "param_4": secure_parameter_value} 16 | 17 | new_value = ''.join(random.choices(string.ascii_lowercase, k = 8)) 18 | 19 | value = get_message_from_lambda(function_name) 20 | assert value == return_value 21 | 22 | update_parameter(ssm_parameter, new_value) 23 | update_parameter(ssm_parameter_2, new_value) 24 | value = get_message_from_lambda(function_name) 25 | assert value == return_value 26 | 27 | time.sleep(10) 28 | value = get_message_from_lambda(function_name) 29 | return_value["param_1"] = new_value 30 | return_value["param_2"] = new_value 31 | assert value == return_value 32 | 33 | # Config Variables 34 | config = get_serverless_config() 35 | service = config['service'] 36 | stage = config['provider']['stage'] -------------------------------------------------------------------------------- /tests/integration_tests/test_s3.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import string 4 | 5 | from tests.variables_data import * 6 | from tests.helper_functions import upload_object 7 | 8 | from tests.integration_tests.helper_functions import get_message_from_lambda, get_serverless_config 9 | from tests.helper_functions import get_s3_bucket_name 10 | 11 | bucket_name = get_s3_bucket_name() 12 | s3_key = "tests/s3/key.json" 13 | 14 | 15 | def generic_test(function_name, ori_status, sleep_time=60): 16 | 17 | status = get_message_from_lambda(function_name) 18 | assert status == ori_status 19 | 20 | upload_object('../test_env/test_data/s3_new.json', bucket_name, s3_key) 21 | status = get_message_from_lambda(function_name) 22 | assert status == ori_status 23 | 24 | time.sleep(sleep_time) 25 | status = get_message_from_lambda(function_name) 26 | assert status == "new" 27 | 28 | upload_object('../test_env/test_data/s3_old.json', bucket_name, s3_key) 29 | status = get_message_from_lambda(function_name) 30 | assert status == "new" 31 | 32 | time.sleep(sleep_time) 33 | status = get_message_from_lambda(function_name) 34 | assert status == ori_status 35 | 36 | 37 | def test_initialize(): 38 | 39 | upload_object('../test_env/test_data/s3_old.json', bucket_name, s3_key) 40 | 41 | 42 | def test_s3_download(): 43 | 44 | function_name = f"{service}-{stage}-s3_download" 45 | generic_test(function_name, "old", 5) 46 | 47 | # Config Variables 48 | config = get_serverless_config() 49 | service = config['service'] 50 | stage = config['provider']['stage'] 51 | -------------------------------------------------------------------------------- /tests/integration_tests/test_secrets.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import string 4 | 5 | from tests.variables_data import * 6 | from tests.helper_functions import update_secret 7 | 8 | from tests.integration_tests.helper_functions import get_message_from_lambda, get_serverless_config 9 | 10 | def generic_test(function_name, secret_name, secret_value, sleep_time=60, secret_type='String'): 11 | 12 | dummy_value = ''.join(random.choices(string.ascii_lowercase, k = 25)) 13 | dummy_value_returned = dummy_value 14 | if secret_type == 'Binary': 15 | secret_value = secret_value.decode('utf-8') 16 | dummy_value = dummy_value.encode('utf-8') 17 | 18 | value = get_message_from_lambda(function_name) 19 | assert value == secret_value 20 | 21 | update_secret(secret_name, dummy_value, secret_type) 22 | value = get_message_from_lambda(function_name) 23 | assert value == secret_value 24 | 25 | time.sleep(sleep_time) 26 | value = get_message_from_lambda(function_name) 27 | assert value == dummy_value_returned 28 | 29 | update_secret(secret_name, secret_value, secret_type) 30 | value = get_message_from_lambda(function_name) 31 | assert value == dummy_value_returned 32 | 33 | time.sleep(sleep_time) 34 | value = get_message_from_lambda(function_name) 35 | assert value == secret_value 36 | 37 | def test_initialize(): 38 | update_secret(secret_name_string, secret_name_string_value, secret_type='String') 39 | update_secret(secret_name_binary, secret_name_binary_value, secret_type='Binary') 40 | 41 | def test_lambda_secret_string(): 42 | function_name = f"{service}-{stage}-secret_string" 43 | generic_test(function_name, secret_name_string ,secret_name_string_value, 5) 44 | 45 | def test_lambda_secret_binary(): 46 | function_name = f"{service}-{stage}-secret_binary" 47 | generic_test(function_name, secret_name_binary ,secret_name_binary_value, 5, secret_type='Binary') 48 | 49 | def test_lambda_default(): 50 | function_name = f"{service}-{stage}-secret_string_default" 51 | generic_test(function_name, secret_name_string ,secret_name_string_value, 60) 52 | 53 | 54 | # Config Variables 55 | config = get_serverless_config() 56 | service = config['service'] 57 | stage = config['provider']['stage'] -------------------------------------------------------------------------------- /tests/test_env/s3.tf: -------------------------------------------------------------------------------- 1 | # Provider Block 2 | provider "aws" { 3 | version = "~> 2.7" 4 | # region -> Taken from env vars or defaulted from profile 5 | } 6 | 7 | resource "aws_s3_bucket" "test_s3bucket" { 8 | bucket_prefix = "lambda-cache" 9 | acl = "private" 10 | force_destroy = true 11 | } 12 | 13 | resource "aws_ssm_parameter" "test_s3bucket_name" { 14 | type = "String" 15 | description = "Name of s3 bucket to hold layer artifacts" 16 | name = "/lambda-cache/s3/bucket_name" 17 | value = aws_s3_bucket.test_s3bucket.bucket 18 | overwrite = true 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/test_env/test_data/s3_new.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "new" 3 | } -------------------------------------------------------------------------------- /tests/test_env/test_data/s3_old.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "old" 3 | } -------------------------------------------------------------------------------- /tests/test_env/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /tests/test_generic.py: -------------------------------------------------------------------------------- 1 | import toml 2 | from lambda_cache import __version__ 3 | 4 | def test_version(): 5 | """ 6 | Test that version in __init.py__ matches pyproject.toml file 7 | """ 8 | with open('../pyproject.toml') as toml_file: 9 | config = toml.load(toml_file) 10 | version = config.get('tool').get('poetry').get('version') 11 | 12 | assert __version__ == version 13 | -------------------------------------------------------------------------------- /tests/test_s3.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import s3 2 | import json 3 | import time 4 | import pytest 5 | 6 | from tests.context_object import LambdaContext 7 | from tests.helper_functions import upload_object, get_s3_bucket_name, check_status_file 8 | 9 | from lambda_cache import s3 10 | from lambda_cache.exceptions import InvalidS3UriError 11 | 12 | bucket_name = get_s3_bucket_name() 13 | s3_key = "tests/s3/key.json" 14 | s3Uri = f"s3://{bucket_name}/{s3_key}" 15 | 16 | 17 | def test_initialize(): 18 | 19 | upload_object('test_env/test_data/s3_old.json', bucket_name, s3_key) 20 | status = check_status_file(bucket_name, s3_key) 21 | assert status == "old" 22 | 23 | 24 | def decorated_function_test(decorated_function, entry_name, ori_status, max_age_in_seconds): 25 | 26 | test_event = {'event_name': 'test'} 27 | test_context = LambdaContext() 28 | new_status = "new" 29 | 30 | return_status = decorated_function(test_event, test_context) 31 | assert return_status == ori_status 32 | 33 | # Update object and download 34 | upload_object('test_env/test_data/s3_new.json', bucket_name, s3_key) 35 | return_status = decorated_function(test_event, test_context) 36 | assert return_status == ori_status 37 | 38 | # Wait max_age_in_seconds call again 39 | time.sleep(max_age_in_seconds) 40 | return_status = decorated_function(test_event, test_context) 41 | assert return_status == new_status 42 | 43 | # Revert back to normal 44 | upload_object('test_env/test_data/s3_old.json', bucket_name, s3_key) 45 | return_status = decorated_function(test_event, test_context) 46 | assert return_status == new_status 47 | 48 | time.sleep(max_age_in_seconds) 49 | return_status = decorated_function(test_event, test_context) 50 | assert return_status == ori_status 51 | 52 | 53 | # Test s3 54 | @s3.cache(s3Uri=f"s3://{bucket_name}/{s3_key}", max_age_in_seconds=5, check_before_download=False) 55 | def s3_download(event, context): 56 | with open(getattr(context, s3_key.split('/')[-1]), 'r') as file_data: 57 | status = json.loads(file_data.read())['status'] 58 | 59 | return status 60 | 61 | # Test Invalid Uri 62 | @s3.cache(s3Uri=f"L3:--/{bucket_name}/{s3_key}", max_age_in_seconds=5, check_before_download=False) 63 | def invalid_uri(event, context): 64 | status = True 65 | return status 66 | 67 | # Test entry_name 68 | @s3.cache(s3Uri=f"s3://{bucket_name}/{s3_key}", entry_name='temp_key.json', max_age_in_seconds=5, check_before_download=False) 69 | def s3_download_entry_name(event, context): 70 | with open("/tmp/temp_key.json") as file_data: 71 | status = json.loads(file_data.read())['status'] 72 | 73 | return status 74 | 75 | # Test default seconds 76 | @s3.cache(s3Uri=f"s3://{bucket_name}/{s3_key}", entry_name='temp_key.json', check_before_download=False) 77 | def s3_default_age(event, context): 78 | with open("/tmp/temp_key.json") as file_data: 79 | status = json.loads(file_data.read())['status'] 80 | 81 | return status 82 | 83 | # Test non updated file 84 | @s3.cache(s3Uri=f"s3://{bucket_name}/{s3_key}", max_age_in_seconds=5, entry_name='temp_key.json', check_before_download=True) 85 | def s3_not_updated(event, context): 86 | with open("/tmp/temp_key.json") as file_data: 87 | status = json.loads(file_data.read())['status'] 88 | 89 | return status 90 | 91 | 92 | def test_decorated_functions(): 93 | decorated_function_test(s3_download, False, max_age_in_seconds=5, ori_status="old") 94 | decorated_function_test(s3_download_entry_name, entry_name="temp_key.json", max_age_in_seconds=5, ori_status="old") 95 | decorated_function_test(s3_download, False, max_age_in_seconds=60, ori_status="old") 96 | not_refreshed_test(s3_not_updated, False, max_age_in_seconds=5, ori_status="old") 97 | 98 | 99 | def test_invalid_s3Uri(): 100 | with pytest.raises(InvalidS3UriError) as e: 101 | invalid_uri({}, LambdaContext) 102 | assert e == "InvalidS3UriError" 103 | 104 | 105 | def test_get_entry(): 106 | file_location = s3.get_entry(s3Uri=f"s3://{bucket_name}/{s3_key}", max_age_in_seconds=5, entry_name=False, check_before_download=True) 107 | with open(file_location, 'r') as file_data: 108 | status = json.loads(file_data.read())['status'] 109 | assert status == "old" 110 | 111 | 112 | def not_refreshed_test(decorated_function, entry_name, ori_status, max_age_in_seconds): 113 | """ 114 | Special use-case where file is not updated to get 100% code coverage 115 | package will only check file's last_modified_date, and not download 116 | """ 117 | 118 | test_event = {'event_name': 'test'} 119 | test_context = LambdaContext() 120 | 121 | return_status = decorated_function(test_event, test_context) 122 | assert return_status == ori_status 123 | 124 | # Update object and download 125 | return_status = decorated_function(test_event, test_context) 126 | assert return_status == ori_status 127 | 128 | # Wait max_age_in_seconds call again 129 | time.sleep(max_age_in_seconds) 130 | return_status = decorated_function(test_event, test_context) 131 | assert return_status == ori_status 132 | 133 | # Wait max_age_in_seconds call again 134 | time.sleep(max_age_in_seconds) 135 | return_status = decorated_function(test_event, test_context) 136 | assert return_status == ori_status -------------------------------------------------------------------------------- /tests/test_secrets_manager_cache.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import secrets_manager 2 | from lambda_cache.exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 3 | from tests.context_object import LambdaContext 4 | 5 | import time 6 | import random, string 7 | import pytest 8 | from botocore.exceptions import ClientError 9 | 10 | from tests.variables_data import * 11 | from tests.helper_functions import update_secret 12 | 13 | def test_initialize(): 14 | update_secret(secret_name_string, secret_name_string_value) 15 | update_secret(secret_name_binary, secret_name_binary_value, secret_type='Binary') 16 | 17 | def test_get_secrets(): 18 | 19 | parameter_assignment(secret_name_string, secret_name_string_value, secret_type='String') 20 | parameter_assignment(secret_name_binary, secret_name_binary_value, secret_type='Binary') 21 | 22 | def test_default_decorators(): 23 | 24 | dummy_value = ''.join(random.choices(string.ascii_lowercase, k = 25)) 25 | decorator_test(secret_name_string, secret_string_default_name, secret_name_string_value, normal_call, ttl=60) 26 | decorator_test(secret_name_string, secret_string_default_name, secret_name_string_value, call_with_ttl, ttl=10) 27 | decorator_test(secret_name_string, "new_secret", secret_name_string_value, call_with_entry_name, ttl=10) 28 | decorator_test(secret_name_binary, "binary_secret", secret_name_binary_value, call_binary, ttl=10, secret_type='Binary') 29 | 30 | def test_invalid_parameters(): 31 | 32 | with pytest.raises(ArgumentTypeNotSupportedError) as e: 33 | secrets_manager.get_entry(name=123, entry_name="dummy") 34 | assert e['Code'] == "ArgumentTypeNotSupportedError" 35 | 36 | with pytest.raises(NoEntryNameError) as e: 37 | secrets_manager.get_entry(name=123) 38 | assert e['Code'] == "NoEntryNameError" 39 | 40 | 41 | # Test parameter assignment 42 | def parameter_assignment(secret_name, secret_value, secret_type='String', ttl=10): 43 | 44 | dummy_value = ''.join(random.choices(string.ascii_lowercase, k = 25)) 45 | if secret_type == 'Binary': 46 | dummy_value = dummy_value.encode('utf-8') 47 | 48 | secret = secrets_manager.get_entry(name=secret_name, max_age_in_seconds=ttl) 49 | assert secret == secret_value 50 | 51 | # update and check, should be old value 52 | update_secret(secret_name, dummy_value, secret_type) 53 | secret = secrets_manager.get_entry(name=secret_name, max_age_in_seconds=ttl) 54 | assert secret == secret_value 55 | 56 | # Wait ttl, previous update should now appear 57 | time.sleep(ttl) 58 | secret = secrets_manager.get_entry(name=secret_name, max_age_in_seconds=ttl) 59 | assert secret == dummy_value 60 | 61 | # Update back to original number, dummy value should still be present in cache 62 | update_secret(secret_name, secret_value, secret_type) 63 | time.sleep(int(ttl/2)) 64 | secret = secrets_manager.get_entry(name=secret_name, max_age_in_seconds=ttl) 65 | assert secret == dummy_value 66 | 67 | # Update back to original number, dummy value should still be present in cache 68 | time.sleep(int(ttl/2)+1) 69 | secret = secrets_manager.get_entry(name=secret_name, max_age_in_seconds=ttl) 70 | assert secret == secret_value 71 | 72 | # Test Parameter Caching TTL settings 73 | @secrets_manager.cache(name=secret_name_string) 74 | def normal_call(event, context): 75 | return context 76 | 77 | @secrets_manager.cache(name=secret_name_string, max_age_in_seconds=10) 78 | def call_with_ttl(event, context): 79 | return context 80 | 81 | @secrets_manager.cache(name=secret_name_string, max_age_in_seconds=10, entry_name="new_secret") 82 | def call_with_entry_name(event, context): 83 | return context 84 | 85 | @secrets_manager.cache(name=secret_name_binary, max_age_in_seconds=10, entry_name="binary_secret") 86 | def call_binary(event, context): 87 | return context 88 | 89 | def decorator_test(name, entry_name, secret_value, decorated_function, ttl=10, secret_type='String'): 90 | 91 | dummy_value = ''.join(random.choices(string.ascii_lowercase, k = 25)) 92 | if secret_type == 'Binary': 93 | dummy_value = dummy_value.encode('utf-8') 94 | 95 | test_event = dict() 96 | test_context = LambdaContext() 97 | 98 | context = decorated_function(test_event ,test_context) 99 | assert getattr(context, entry_name) == secret_value 100 | 101 | update_secret(name, dummy_value, secret_type=secret_type) 102 | context = decorated_function(test_event ,test_context) 103 | assert getattr(context, entry_name) == secret_value 104 | 105 | time.sleep(ttl) 106 | context = decorated_function(test_event ,test_context) 107 | assert getattr(context, entry_name) == dummy_value 108 | 109 | update_secret(name, secret_value, secret_type=secret_type) 110 | context = decorated_function(test_event ,test_context) 111 | assert getattr(context, entry_name) == dummy_value 112 | 113 | time.sleep(ttl) 114 | context = decorated_function(test_event ,test_context) 115 | assert getattr(context, entry_name) == secret_value -------------------------------------------------------------------------------- /tests/test_ssm_cache.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import ssm 2 | from lambda_cache.exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 3 | from tests.context_object import LambdaContext 4 | 5 | import time 6 | import random, string 7 | import pytest 8 | from botocore.exceptions import ClientError 9 | import toml 10 | 11 | from tests.variables_data import * 12 | from tests.helper_functions import update_parameter 13 | 14 | 15 | def test_initialize(): 16 | update_parameter(ssm_parameter, ssm_parameter_value) 17 | update_parameter(ssm_parameter_2, ssm_parameter_2_value) 18 | update_parameter(secure_parameter, secure_parameter_value, param_type='SecureString') 19 | update_parameter(long_name_parameter, long_name_value) 20 | update_parameter(string_list_parameter,string_list_value, param_type='StringList') 21 | 22 | # Test parameter import and stacking 23 | @ssm.cache(parameter=ssm_parameter) 24 | def single_var_handler(event, context): 25 | return context 26 | 27 | @ssm.cache(parameter=ssm_parameter) 28 | @ssm.cache(parameter=ssm_parameter_2) 29 | def double_var_handler(event, context): 30 | return context 31 | 32 | @ssm.cache(parameter=long_name_parameter) 33 | def long_name_var_handler(event,context): 34 | return context 35 | 36 | def test_var_handlers(): 37 | 38 | test_event = {'event_name': 'test'} 39 | test_context = LambdaContext() 40 | 41 | context = single_var_handler(test_event, test_context) 42 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 43 | 44 | context = double_var_handler(test_event, test_context) 45 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 46 | assert getattr(context, ssm_parameter_2_default_name) == ssm_parameter_2_value 47 | 48 | context = long_name_var_handler(test_event, test_context) 49 | assert getattr(context, long_name_default_name) == long_name_value 50 | 51 | 52 | # Test Parameter Caching TTL settings 53 | @ssm.cache(parameter=ssm_parameter, max_age_in_seconds=5) 54 | def five_second_ttl(event, context): 55 | return context 56 | 57 | # Test Parameter Rename 58 | @ssm.cache(parameter=ssm_parameter, entry_name=ssm_parameter_replaced_var_name, max_age_in_seconds=5) 59 | def renamed_var(event, context): 60 | return context 61 | 62 | # Test Secure String import 63 | @ssm.cache(parameter=secure_parameter, max_age_in_seconds=5) 64 | def secure_var_handler(event,context): 65 | return context 66 | 67 | # Test StringList with cache 68 | @ssm.cache(parameter=string_list_parameter, max_age_in_seconds=5) 69 | def string_list(event, context): 70 | return context 71 | 72 | def test_decorated_functions(): 73 | 74 | decorator_test(five_second_ttl, ssm_parameter, ssm_parameter_default_name, ssm_parameter_value, 5) 75 | decorator_test(renamed_var, ssm_parameter, ssm_parameter_replaced_var_name, ssm_parameter_value, 5) 76 | decorator_test(secure_var_handler,secure_parameter, secure_parameter_default_name, secure_parameter_value, 5, param_type='SecureString') 77 | decorator_test(string_list, string_list_parameter,string_list_default_name, string_list_value.split(','), 5, param_type='StringList') 78 | 79 | def decorator_test(decorated_function, parameter_name, entry_name, parameter_value, max_age_in_seconds, param_type='String'): 80 | 81 | dummy_value_updated = ''.join(random.choices(string.ascii_lowercase, k = 25)) 82 | dummy_value_returned = dummy_value_updated 83 | revert_parameter_value = parameter_value 84 | if param_type == 'StringList': 85 | dummy_value_updated = ','.join(random.choices(string.ascii_lowercase, k = 5)) 86 | dummy_value_returned = dummy_value_updated.split(',') 87 | revert_parameter_value = ','.join(parameter_value) 88 | 89 | test_event = {'event_name': 'test'} 90 | test_context = LambdaContext() 91 | 92 | context = decorated_function(test_event, test_context) 93 | assert getattr(context, entry_name) == parameter_value 94 | 95 | # Update parameter but call before max_age_in_seconds 96 | update_parameter(parameter_name, dummy_value_updated, param_type=param_type) 97 | context = decorated_function(test_event, test_context) 98 | assert getattr(context, entry_name) == parameter_value 99 | 100 | # Wait max_age_in_seconds call again 101 | time.sleep(max_age_in_seconds) 102 | context = decorated_function(test_event, test_context) 103 | assert getattr(context, entry_name) == dummy_value_returned 104 | 105 | # Revert back to normal 106 | update_parameter(parameter_name, revert_parameter_value, param_type=param_type) 107 | time.sleep(max_age_in_seconds) 108 | context = decorated_function(test_event, test_context) 109 | assert getattr(context, entry_name) == parameter_value 110 | 111 | 112 | 113 | # Test Parameter Caching TTL settings 114 | @ssm.cache(parameter=ssm_parameter, max_age_in_seconds=10) 115 | def invalidate_cache(event, context): 116 | if event.get('refresh', False): 117 | result = ssm.get_entry(ssm_parameter, max_age_in_seconds=0) 118 | setattr(context, ssm_parameter_default_name, result) 119 | return context 120 | 121 | def test_invalidate_cache(): 122 | 123 | updated_value = 'Dummy Value NEW!!' 124 | test_event = {} 125 | test_context = LambdaContext() 126 | refresh_event = {'refresh': True} 127 | 128 | context = invalidate_cache({}, test_context) 129 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 130 | 131 | # Update parameter and test within 5 seconds 132 | update_parameter(ssm_parameter, updated_value) 133 | time.sleep(5) 134 | context = invalidate_cache({}, test_context) 135 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 136 | 137 | # Wait 5 seconds call again, parameter should be refreshed 138 | time.sleep(5) 139 | context = invalidate_cache({}, test_context) 140 | assert getattr(context, ssm_parameter_default_name) == updated_value 141 | 142 | # Update parameter back to ssm_parameter_value, but call with invalidated cache 143 | update_parameter(ssm_parameter, ssm_parameter_value) 144 | context = invalidate_cache(refresh_event, test_context) 145 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 146 | 147 | # Test max_age_in_seconds=0 settings, no cache 148 | @ssm.cache(parameter=ssm_parameter, max_age_in_seconds=0) 149 | def no_cache(event, context): 150 | return context 151 | 152 | def test_no_cache(): 153 | test_event = {'event_name': 'test'} 154 | test_context = LambdaContext() 155 | new_value = "New Value" 156 | 157 | context = no_cache(test_event, test_context) 158 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value 159 | 160 | update_parameter(ssm_parameter, new_value) 161 | context = no_cache(test_event, test_context) 162 | assert getattr(context, ssm_parameter_default_name) == new_value 163 | 164 | update_parameter(ssm_parameter, ssm_parameter_value) 165 | context = no_cache(test_event, test_context) 166 | assert getattr(context, ssm_parameter_default_name) == ssm_parameter_value -------------------------------------------------------------------------------- /tests/test_ssm_invalid.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import ssm 2 | from lambda_cache.exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 3 | from tests.context_object import LambdaContext 4 | 5 | import pytest 6 | from botocore.exceptions import ClientError 7 | import toml 8 | 9 | from tests.variables_data import * 10 | from tests.helper_functions import update_parameter 11 | 12 | 13 | # Test Non-existent parameter 14 | 15 | @ssm.cache(parameter="/some/nonexist/parameter") 16 | def parameter_not_exist_var_handler(event, context): 17 | return context 18 | 19 | def test_non_existing_parameter(): 20 | 21 | test_event = {'event_name': 'test'} 22 | test_context = LambdaContext() 23 | with pytest.raises(ClientError) as e: 24 | context = parameter_not_exist_var_handler(test_event, test_context) 25 | assert e['Error']['Code'] == "ParameterNotFound" 26 | 27 | # Test invalid parameter 28 | @ssm.cache(parameter=123, entry_name="hello") 29 | def invalid_parameter(event, context): 30 | return context 31 | 32 | def test_invalid_parameter(): 33 | 34 | with pytest.raises(ArgumentTypeNotSupportedError) as e: 35 | invalid_parameter({}, LambdaContext()) 36 | assert e['Code'] == "ArgumentTypeNotSupportedError" 37 | 38 | with pytest.raises(NoEntryNameError) as e: 39 | ssm.get_entry(parameter=123, max_age_in_seconds=4) 40 | assert e['Code'] == "NoEntryNameError" 41 | 42 | with pytest.raises(NoEntryNameError) as e: 43 | ssm.get_entry(parameter={'dummy': 'dict'}, max_age_in_seconds=2) 44 | assert e['Code'] == "NoEntryNameError" 45 | 46 | with pytest.raises(ArgumentTypeNotSupportedError) as e: 47 | ssm.get_entry(parameter=123, max_age_in_seconds=2, entry_name="hello") 48 | assert e['Code'] == "ArgumentTypeNotSupportedError" 49 | 50 | with pytest.raises(ArgumentTypeNotSupportedError) as e: 51 | ssm.get_entry(parameter=(123,123), max_age_in_seconds=2, entry_name="hello") 52 | assert e['Code'] == "ArgumentTypeNotSupportedError" 53 | 54 | with pytest.raises(NoEntryNameError) as e: 55 | ssm.get_entry(parameter=['abc','_'], max_age_in_seconds=2) 56 | assert e['Code'] == "NoEntryNameError" 57 | 58 | 59 | # Test Non-existent parameter 60 | @ssm.cache(parameter="/some/nonexist/parameter") 61 | def parameter_not_exist_var_handler(event, context): 62 | return context 63 | 64 | def test_non_existing_parameter(): 65 | 66 | test_event = {'event_name': 'test'} 67 | test_context = LambdaContext() 68 | with pytest.raises(ClientError) as e: 69 | context = parameter_not_exist_var_handler(test_event, test_context) 70 | assert e['Error']['Code'] == "ParameterNotFound" -------------------------------------------------------------------------------- /tests/test_ssm_multi_param.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import ssm 2 | from lambda_cache.exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 3 | from tests.context_object import LambdaContext 4 | 5 | import time 6 | import random, string 7 | import pytest 8 | from botocore.exceptions import ClientError 9 | import toml 10 | 11 | from tests.variables_data import * 12 | from tests.helper_functions import update_parameter 13 | from tests.test_ssm_cache import test_initialize 14 | 15 | # Test get_parameters 16 | @ssm.cache(parameter=[ssm_parameter, ssm_parameter_2, string_list_parameter, secure_parameter], entry_name=default_entry_name, max_age_in_seconds=10) 17 | def multi_parameters(event, context): 18 | return context 19 | 20 | def test_multi_parameters(): 21 | 22 | dummy_string = "__" 23 | dummy_list = "-,--,---" 24 | test_event = {} 25 | test_context = LambdaContext() 26 | 27 | context = multi_parameters(test_event,test_context) 28 | assert getattr(context, default_entry_name).get(ssm_parameter_default_name) == ssm_parameter_value 29 | assert getattr(context, default_entry_name).get(ssm_parameter_2_default_name) == ssm_parameter_2_value 30 | assert getattr(context, default_entry_name).get(string_list_default_name) == string_list_value.split(',') 31 | assert getattr(context, default_entry_name).get(secure_parameter_default_name) == secure_parameter_value 32 | 33 | update_parameter(ssm_parameter, dummy_string) 34 | update_parameter(ssm_parameter_2, dummy_string) 35 | update_parameter(secure_parameter, dummy_string, param_type='SecureString') 36 | update_parameter(string_list_parameter, dummy_list, param_type='StringList') 37 | context = multi_parameters(test_event,test_context) 38 | assert getattr(context, default_entry_name).get(ssm_parameter_default_name) == ssm_parameter_value 39 | assert getattr(context, default_entry_name).get(ssm_parameter_2_default_name) == ssm_parameter_2_value 40 | assert getattr(context, default_entry_name).get(string_list_default_name) == string_list_value.split(',') 41 | assert getattr(context, default_entry_name).get(secure_parameter_default_name) == secure_parameter_value 42 | 43 | time.sleep(10) 44 | context = multi_parameters(test_event,test_context) 45 | assert getattr(context, default_entry_name).get(ssm_parameter_default_name) == dummy_string 46 | assert getattr(context, default_entry_name).get(ssm_parameter_2_default_name) == dummy_string 47 | assert getattr(context, default_entry_name).get(string_list_default_name) == dummy_list.split(',') 48 | assert getattr(context, default_entry_name).get(secure_parameter_default_name) == dummy_string 49 | 50 | test_initialize() 51 | context = multi_parameters(test_event,test_context) 52 | assert getattr(context, default_entry_name).get(ssm_parameter_default_name) == dummy_string 53 | assert getattr(context, default_entry_name).get(ssm_parameter_2_default_name) == dummy_string 54 | assert getattr(context, default_entry_name).get(string_list_default_name) == dummy_list.split(',') 55 | assert getattr(context, default_entry_name).get(secure_parameter_default_name) == dummy_string 56 | 57 | time.sleep(10) 58 | context = multi_parameters(test_event,test_context) 59 | assert getattr(context, default_entry_name).get(ssm_parameter_default_name) == ssm_parameter_value 60 | assert getattr(context, default_entry_name).get(ssm_parameter_2_default_name) == ssm_parameter_2_value 61 | assert getattr(context, default_entry_name).get(string_list_default_name) == string_list_value.split(',') 62 | assert getattr(context, default_entry_name).get(secure_parameter_default_name) == secure_parameter_value -------------------------------------------------------------------------------- /tests/test_ssm_param_assignment.py: -------------------------------------------------------------------------------- 1 | from lambda_cache import ssm 2 | from lambda_cache.exceptions import ArgumentTypeNotSupportedError, NoEntryNameError 3 | from tests.context_object import LambdaContext 4 | 5 | import time 6 | import random, string 7 | import pytest 8 | from botocore.exceptions import ClientError 9 | import toml 10 | 11 | from tests.variables_data import * 12 | from tests.helper_functions import update_parameter 13 | from tests.test_ssm_cache import test_initialize 14 | 15 | # Test parameter assignment 16 | def test_get_parameter(): 17 | 18 | parameter_assignment(ssm_parameter, ssm_parameter_value) 19 | parameter_assignment(secure_parameter, secure_parameter_value, 'SecureString') 20 | parameter_assignment(string_list_parameter, string_list_value, parameter_type='StringList') 21 | 22 | def parameter_assignment(parameter, parameter_value, parameter_type='String'): 23 | 24 | dummy_name = ''.join(random.choices(string.ascii_lowercase, k = 8)) 25 | 26 | if parameter_type == 'StringList': 27 | dummy_value = "d,e,f" 28 | dummy_value_return = dummy_value.split(",") 29 | parameter_value_return = parameter_value.split(",") 30 | else: 31 | dummy_value = 'get_dummy' 32 | dummy_value_return = dummy_value 33 | parameter_value_return = parameter_value 34 | 35 | ttl = 5 36 | 37 | param = ssm.get_entry(parameter=parameter, max_age_in_seconds=ttl) 38 | assert param == parameter_value_return 39 | 40 | update_parameter(parameter, dummy_value, parameter_type) 41 | param = ssm.get_entry(parameter=parameter, max_age_in_seconds=ttl) 42 | assert param == parameter_value_return 43 | 44 | time.sleep(ttl) 45 | param = ssm.get_entry(parameter=parameter, max_age_in_seconds=ttl) 46 | assert param == dummy_value_return 47 | param = ssm.get_entry(parameter=parameter, max_age_in_seconds=ttl) 48 | assert param == dummy_value_return 49 | 50 | time.sleep(ttl) 51 | update_parameter(parameter, parameter_value, parameter_type) 52 | param = ssm.get_entry(parameter=parameter, max_age_in_seconds=ttl) 53 | assert param == parameter_value_return -------------------------------------------------------------------------------- /tests/variables_data.py: -------------------------------------------------------------------------------- 1 | ssm_parameter = "/lambda_cache/something" 2 | ssm_parameter_value = "Dummy Value 1" 3 | ssm_parameter_default_name = "something" 4 | ssm_parameter_replaced_var_name = "variable_1" 5 | 6 | ssm_parameter_2 = "/lambda_cache/test/something_else" 7 | ssm_parameter_2_value = "Dummy Value 2" 8 | ssm_parameter_2_default_name = "something_else" 9 | 10 | secure_parameter = "/lambda_cache/test/secure/somethingsecure" 11 | secure_parameter_value = "This is secure" 12 | secure_parameter_default_name = "somethingsecure" 13 | 14 | long_name_parameter = "/lambda_cache/test/this/is/a/long/parameter/value/zzzzzzz/9/10/11" 15 | long_name_value = "Long name" 16 | long_name_default_name = "11" 17 | 18 | string_list_parameter = "/lambda/cache/test/somelist" 19 | string_list_value = "a,b,c" 20 | string_list_default_name = "somelist" 21 | 22 | # Secret variables 23 | secret_name_string = "/lambda_cache/test/secret_string" 24 | secret_name_string_value = "This is a secret /@#$" 25 | secret_string_default_name = "secret_string" 26 | 27 | secret_name_binary = "/lambda_cache/test/secret_binary" 28 | secret_name_binary_value = "this is a secret in binary".encode('utf-8') 29 | secret_binary_default_name = "secret_binary" 30 | 31 | default_entry_name = "context_lambda_cache" 32 | 33 | # S3 variables 34 | s3_key = "tests/s3/key.json" 35 | s3_bucket_ssm_param = "/lambda-cache/s3/bucket_name" --------------------------------------------------------------------------------