├── .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 |   
5 |
6 |   
7 |
8 |  [](https://coveralls.io/github/keithrozario/lambda-cache?branch=release) [](https://www.codacy.com/manual/keithrozario/lambda-cache?utm_source=github.com&utm_medium=referral&utm_content=keithrozario/lambda-cache&utm_campaign=Badge_Grade) [](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 | 
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 | 
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"
--------------------------------------------------------------------------------