├── .gitignore ├── {{ cookiecutter.project_name }} ├── tests │ ├── __init__.py │ ├── unit │ │ ├── __init__.py │ │ └── test_hello_function.py │ └── conftest.py ├── hello_world │ ├── __init__.py │ ├── requirements.txt │ └── app.py ├── .coveragerc ├── Pipfile ├── Makefile ├── events │ ├── hello_name.json │ └── hello.json ├── template.yaml ├── .gitignore ├── README.md └── Pipfile.lock ├── .env ├── cookiecutter.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── hooks └── post_gen_project ├── LICENSE ├── README.md └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .mypy_cache 3 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/hello_world/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | POWERTOOLS_METRICS_NAMESPACE="MyServerlessApplication" 2 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Name of the project", 3 | "include_safe_deployment": "n" 4 | } -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = */build/*,tests/* 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | raise NotImplementedError.* 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | 7 | [packages] 8 | "aws-lambda-powertools[tracer]" = "*" 9 | 10 | [dev-packages] 11 | 12 | boto3 = "*" 13 | boto3-stubs = "*" 14 | pytest = "*" 15 | pytest-cov = "*" 16 | 17 | [requires] 18 | python_version = "3.9" 19 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/hello_world/requirements.txt: -------------------------------------------------------------------------------- 1 | # Those dependencies can be omitted: 2 | # - boto3 is part of Lambda runtime for Python 3 | # - aws-lambda-powertools is embedded as a layer (see SAM template) 4 | # This is always a good practice as your lambda packaged code will be much lighter, and it will be invoked quicker 5 | 6 | # boto3 7 | # aws-lambda-powertools 8 | -------------------------------------------------------------------------------- /hooks/post_gen_project: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function 3 | 4 | import os 5 | 6 | TERMINATOR = "\x1b[0m" 7 | INFO = "\x1b[1;33m [INFO]: " 8 | SUCCESS = "\x1b[1;32m [SUCCESS]: " 9 | HINT = "\x1b[3;33m" 10 | 11 | def main(): 12 | 13 | project_name = '{{ cookiecutter.project_name }}' 14 | 15 | print(SUCCESS + 16 | "Project initialized successfully! You can now jump to {} folder". 17 | format(project_name) + TERMINATOR) 18 | print(INFO + 19 | "{}/README.md contains instructions on how to proceed.". 20 | format(project_name) + TERMINATOR) 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/tests/unit/test_hello_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from hello_world import app 4 | 5 | 6 | def test_lambda_handler_hello_path(apigw_hello_event, lambda_context): 7 | ret = app.lambda_handler(apigw_hello_event, lambda_context) 8 | expected = json.dumps({"message": "hello unknown!"}, separators=(",", ":")) 9 | 10 | assert ret["statusCode"] == 200 11 | assert ret["body"] == expected 12 | 13 | 14 | def test_lambda_handler_hello_you_path(apigw_hello_name_event, lambda_context): 15 | ret = app.lambda_handler(apigw_hello_name_event, lambda_context) 16 | expected = json.dumps({"message": "hello you!"}, separators=(",", ":")) 17 | 18 | assert ret["statusCode"] == 200 19 | assert ret["body"] == expected 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from uuid import uuid4 3 | 4 | import pytest 5 | 6 | 7 | class MockContext(object): 8 | def __init__(self, function_name): 9 | self.function_name = function_name 10 | self.function_version = "v$LATEST" 11 | self.memory_limit_in_mb = 512 12 | self.invoked_function_arn = ( 13 | f"arn:aws:lambda:us-east-1:ACCOUNT:function:{self.function_name}" 14 | ) 15 | self.aws_request_id = str(uuid4) 16 | 17 | 18 | @pytest.fixture 19 | def lambda_context(): 20 | return MockContext("dummy_function") 21 | 22 | 23 | @pytest.fixture() 24 | def apigw_hello_event(): 25 | """Generates API GW Event""" 26 | with open("./events/hello.json", "r") as fp: 27 | return json.load(fp) 28 | 29 | 30 | @pytest.fixture() 31 | def apigw_hello_name_event(): 32 | """Generates API GW Event""" 33 | with open("./events/hello_name.json", "r") as fp: 34 | return json.load(fp) 35 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/hello_world/app.py: -------------------------------------------------------------------------------- 1 | from aws_lambda_powertools import Logger, Metrics, Tracer 2 | from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver 3 | from aws_lambda_powertools.logging import correlation_paths 4 | from aws_lambda_powertools.metrics import MetricUnit 5 | 6 | logger = Logger(service="APP") 7 | tracer = Tracer(service="APP") 8 | metrics = Metrics(namespace="MyApp", service="APP") 9 | app = ApiGatewayResolver() 10 | 11 | 12 | @app.get("/hello/") 13 | @tracer.capture_method 14 | def hello_name(name): 15 | tracer.put_annotation(key="User", value=name) 16 | logger.info(f"Request from {name} received") 17 | metrics.add_metric(name="SuccessfulGreetings", unit=MetricUnit.Count, value=1) 18 | return {"message": f"hello {name}!"} 19 | 20 | 21 | @app.get("/hello") 22 | @tracer.capture_method 23 | def hello(): 24 | tracer.put_annotation(key="User", value="unknown") 25 | logger.info("Request from unknown received") 26 | metrics.add_metric(name="SuccessfulGreetings", unit=MetricUnit.Count, value=1) 27 | return {"message": "hello unknown!"} 28 | 29 | 30 | @tracer.capture_lambda_handler 31 | @logger.inject_lambda_context( 32 | correlation_id_path=correlation_paths.API_GATEWAY_REST, log_event=True 33 | ) 34 | @metrics.log_metrics(capture_cold_start_metric=True) 35 | def lambda_handler(event, context): 36 | try: 37 | return app.resolve(event, context) 38 | except Exception as e: 39 | logger.exception(e) 40 | raise 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cookiecutter SAM for Python Lambda functions 2 | 3 | ## Archived 4 | 5 | This project is now archived. Please use the official AWS SAM CLI template instead. 6 | 7 | ```bash 8 | sam init --app-template hello-world-powertools-python --name sam-app --package-type Zip --runtime python3.12 --no-tracing 9 | ``` 10 | 11 | --- 12 | 13 | This is a [Cookiecutter](https://github.com/audreyr/cookiecutter) template to create a Serverless App based on Serverless Application Model (SAM) and Python 3.9. 14 | 15 | It is important to note that you should not try to `git clone` this project but use `SAM` CLI instead as ``{{cookiecutter.project_slug}}`` will be rendered based on your input and therefore all variables and files will be rendered properly. 16 | 17 | ## Usage 18 | 19 | Generate a new SAM based Serverless App: `sam init --location gh:aws-samples/cookiecutter-aws-sam-python` 20 | 21 | You'll be prompted a few questions to help this cookiecutter template to scaffold this project and after its completed you should see a new folder at your current path with the name of the project you gave as input. 22 | 23 | ## Options 24 | 25 | Option | Description 26 | ------------------------------------------------- | --------------------------------------------------------------------------------- 27 | `include_safe_deployment` | Sends by default 10% of traffic for every 1 minute to a newly deployed function using [CodeDeploy + SAM integration](https://github.com/awslabs/serverless-application-model/blob/master/docs/safe_lambda_deployments.rst) - Linear10PercentEvery1Minute 28 | 29 | # Credits 30 | 31 | * This project has been generated with [Cookiecutter](https://github.com/audreyr/cookiecutter) 32 | * [Bruno Alla's Lambda function template](https://github.com/browniebroke/cookiecutter-lambda-function) 33 | 34 | License 35 | ------- 36 | 37 | This project is licensed under the terms of the [MIT License with no attribution](/LICENSE) 38 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/Makefile: -------------------------------------------------------------------------------- 1 | BASE := $(shell /bin/pwd) 2 | CODE_COVERAGE = 72 3 | PIPENV ?= pipenv 4 | 5 | ############# 6 | # SAM vars # 7 | ############# 8 | 9 | # Name of Docker Network to connect to 10 | # Helpful when you're running Amazon DynamoDB local etc. 11 | NETWORK = "" 12 | 13 | target: 14 | $(info ${HELP_MESSAGE}) 15 | @exit 0 16 | 17 | clean: ##=> Deletes current build environment and latest build 18 | $(info [*] Who needs all that anyway? Destroying environment....) 19 | rm -rf ./.aws-sam/ 20 | 21 | all: clean build 22 | 23 | install: 24 | $(info [*] Installing pipenv) 25 | @pip install pipenv --upgrade 26 | $(MAKE) dev 27 | 28 | dev: 29 | $(info [*] Installing pipenv project dependencies) 30 | @$(PIPENV) install 31 | @$(PIPENV) install -d 32 | 33 | shell: 34 | @$(PIPENV) shell 35 | 36 | build: ##=> Same as package except that we don't create a ZIP 37 | sam build --use-container 38 | 39 | deploy.guided: ##=> Guided deploy that is typically run for the first time only 40 | sam deploy --guided 41 | 42 | deploy: ##=> Deploy app using previously saved SAM CLI configuration 43 | sam deploy 44 | 45 | invoke: ##=> Run SAM Local function with a given event payload 46 | @sam local invoke HelloWorldFunction --event events/hello.json 47 | 48 | run: ##=> Run SAM Local API GW and can optionally run new containers connected to a defined network 49 | @test -z ${NETWORK} \ 50 | && sam local start-api \ 51 | || sam local start-api --docker-network ${NETWORK} 52 | 53 | test: ##=> Run pytest 54 | POWERTOOLS_METRICS_NAMESPACE="MyServerlessApplication" $(PIPENV) run python -m pytest --cov . --cov-report term-missing --cov-fail-under $(CODE_COVERAGE) tests/ -vv 55 | 56 | ci: ##=> Run full workflow - Install deps, build deps, and deploy 57 | $(MAKE) dev 58 | $(MAKE) build 59 | $(MAKE) deploy 60 | 61 | hurry: ##=> Run full workflow for the first time 62 | $(MAKE) install 63 | $(MAKE) build 64 | $(MAKE) deploy.guided 65 | 66 | ############# 67 | # Helpers # 68 | ############# 69 | 70 | define HELP_MESSAGE 71 | Environment variables to be aware of or to hardcode depending on your use case: 72 | 73 | NETWORK 74 | Default: "" 75 | Info: Docker Network to connect to when running Lambda function locally 76 | 77 | Common usage: 78 | 79 | ...::: Installs Pipenv, application and dev dependencies defined in Pipfile :::... 80 | $ make install 81 | 82 | ...::: Builds Lambda function dependencies:::... 83 | $ make build 84 | 85 | ...::: Deploy for the first time :::... 86 | $ make deploy.guided 87 | 88 | ...::: Deploy subsequent changes :::... 89 | $ make deploy 90 | 91 | ...::: Run SAM Local API Gateway :::... 92 | $ make run 93 | 94 | ...::: Run Pytest under tests/ with pipenv :::... 95 | $ make test 96 | 97 | ...::: Spawn a virtual environment shell :::... 98 | $ make shell 99 | 100 | ...::: Cleans up the environment - Deletes Virtualenv, ZIP builds and Dev env :::... 101 | $ make clean 102 | 103 | ...::: Run full workflow from installing Pipenv, dev and app deps, build, and deploy :::... 104 | $ make ci 105 | 106 | Advanced usage: 107 | 108 | ...::: Run SAM Local API Gateway within a Docker Network :::... 109 | $ make run NETWORK="sam-network" 110 | endef 111 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/cookiecutter-aws-sam-python/issues), or [recently closed](https://github.com/aws-samples/cookiecutter-aws-sam-python/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/lambda-edge-hmac-signing/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Licensing 54 | 55 | See the [LICENSE](https://github.com/aws-samples/lambda-edge-hmac-signing/blob/master/LICENSE) file for our project's licensing. We will ask you confirm the licensing of your contribution. 56 | 57 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 58 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/events/hello_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "hello", 3 | "resource": "/hello", 4 | "path": "/hello/you", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "headers": { 8 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 9 | "Accept-Encoding": "gzip, deflate, sdch", 10 | "Accept-Language": "en-US,en;q=0.8", 11 | "Cache-Control": "max-age=0", 12 | "CloudFront-Forwarded-Proto": "https", 13 | "CloudFront-Is-Desktop-Viewer": "true", 14 | "CloudFront-Is-Mobile-Viewer": "false", 15 | "CloudFront-Is-SmartTV-Viewer": "false", 16 | "CloudFront-Is-Tablet-Viewer": "false", 17 | "CloudFront-Viewer-Country": "US", 18 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 19 | "Upgrade-Insecure-Requests": "1", 20 | "User-Agent": "Custom User Agent String", 21 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 22 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 23 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 24 | "X-Forwarded-Port": "443", 25 | "X-Forwarded-Proto": "https" 26 | }, 27 | "multiValueHeaders": { 28 | "Accept": [ 29 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 30 | ], 31 | "Accept-Encoding": [ 32 | "gzip, deflate, sdch" 33 | ], 34 | "Accept-Language": [ 35 | "en-US,en;q=0.8" 36 | ], 37 | "Cache-Control": [ 38 | "max-age=0" 39 | ], 40 | "CloudFront-Forwarded-Proto": [ 41 | "https" 42 | ], 43 | "CloudFront-Is-Desktop-Viewer": [ 44 | "true" 45 | ], 46 | "CloudFront-Is-Mobile-Viewer": [ 47 | "false" 48 | ], 49 | "CloudFront-Is-SmartTV-Viewer": [ 50 | "false" 51 | ], 52 | "CloudFront-Is-Tablet-Viewer": [ 53 | "false" 54 | ], 55 | "CloudFront-Viewer-Country": [ 56 | "US" 57 | ], 58 | "Host": [ 59 | "0123456789.execute-api.us-east-1.amazonaws.com" 60 | ], 61 | "Upgrade-Insecure-Requests": [ 62 | "1" 63 | ], 64 | "User-Agent": [ 65 | "Custom User Agent String" 66 | ], 67 | "Via": [ 68 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 69 | ], 70 | "X-Amz-Cf-Id": [ 71 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 72 | ], 73 | "X-Forwarded-For": [ 74 | "127.0.0.1, 127.0.0.2" 75 | ], 76 | "X-Forwarded-Port": [ 77 | "443" 78 | ], 79 | "X-Forwarded-Proto": [ 80 | "https" 81 | ] 82 | }, 83 | "requestContext": { 84 | "accountId": "123456789012", 85 | "resourceId": "123456", 86 | "stage": "Prod", 87 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 88 | "requestTime": "25/Jul/2020:12:34:56 +0000", 89 | "requestTimeEpoch": 1428582896000, 90 | "identity": { 91 | "cognitoIdentityPoolId": null, 92 | "accountId": null, 93 | "cognitoIdentityId": null, 94 | "caller": null, 95 | "accessKey": null, 96 | "sourceIp": "127.0.0.1", 97 | "cognitoAuthenticationType": null, 98 | "cognitoAuthenticationProvider": null, 99 | "userArn": null, 100 | "userAgent": "Custom User Agent String", 101 | "user": null 102 | }, 103 | "path": "/Prod/hello/you", 104 | "resourcePath": "/hello/you", 105 | "httpMethod": "POST", 106 | "apiId": "1234567890", 107 | "protocol": "HTTP/1.1" 108 | } 109 | } -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/events/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "hello", 3 | "resource": "/hello", 4 | "path": "/hello", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "hello": "/hello" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "Prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "25/Jul/2020:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/Prod/hello", 118 | "resourcePath": "/hello", 119 | "httpMethod": "POST", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | {{ cookiecutter.project_name }} 5 | 6 | Globals: # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html 7 | Api: 8 | EndpointConfiguration: REGIONAL 9 | TracingEnabled: true 10 | Cors: # https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/#cors 11 | # AllowOrigin: "'https://example.com'" 12 | AllowOrigin: "'*'" # Dev only 13 | AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" 14 | MaxAge: "'300'" 15 | BinaryMediaTypes: # https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/#binary-responses 16 | - '*~1*' # converts to */* for any binary type 17 | Function: 18 | Timeout: 5 19 | MemorySize: 256 20 | Runtime: python3.9 21 | Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html 22 | {%- if cookiecutter.include_safe_deployment == "y" %} 23 | AutoPublishAlias: live # More info about Safe Deployments: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-deploymentpreference.html 24 | DeploymentPreference: 25 | Type: Linear10PercentEvery1Minute {% endif %} 26 | # Embed Lambda Powertools as a shared Layer 27 | # See: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer 28 | Layers: # 29 | - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:9 30 | Environment: 31 | Variables: 32 | # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables 33 | LOG_LEVEL: INFO 34 | POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 35 | POWERTOOLS_LOGGER_LOG_EVENT: true 36 | POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication 37 | POWERTOOLS_SERVICE_NAME: hello 38 | 39 | Resources: 40 | HelloWorldFunction: 41 | Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html 42 | Properties: 43 | Handler: app.lambda_handler 44 | CodeUri: hello_world 45 | Description: Hello World function 46 | Events: 47 | HelloPath: 48 | Type: Api # More info about API Event Source: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html 49 | Properties: 50 | Path: /hello 51 | Method: GET 52 | HelloYou: 53 | Type: Api 54 | Properties: 55 | Path: /hello/{name} 56 | Method: GET 57 | # Policies: # Example inline policy 58 | # - Version: "2012-10-17" 59 | # Statement: 60 | # - Effect: "Allow" 61 | # Action: 62 | # - "ssm:GetParameter" 63 | # Resource: 64 | # - "*" 65 | Environment: 66 | Variables: 67 | PARAM1: VALUE 68 | Tags: 69 | LambdaPowertools: python 70 | 71 | # Sample policy to add additional permissions to your Lambda 72 | # HelloWorldFunctionAdditionalPermission: 73 | # Type: "AWS::IAM::Policy" 74 | # Properties: 75 | # PolicyName: "root" 76 | # PolicyDocument: 77 | # Version: "2012-10-17" 78 | # Statement: 79 | # - 80 | # Effect: "Allow" 81 | # Action: "ssm:GetParameters" 82 | # Resource: "*" 83 | # Roles: 84 | # - !Ref HelloWorldFunctionRole # Sample policy to demonstrate Implicit IAM Role created with SAM 85 | 86 | Outputs: 87 | HelloWorldApigwURL: 88 | Description: "API Gateway endpoint URL for Prod environment for Hello World Function" 89 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" 90 | 91 | HelloWorldFunction: 92 | Description: "Hello World Lambda Function ARN" 93 | Value: !GetAtt HelloWorldFunction.Arn 94 | 95 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | /*.zip 138 | 139 | # PyInstaller 140 | # Usually these files are written by a python script from a template 141 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 142 | *.manifest 143 | *.spec 144 | 145 | # Installer logs 146 | pip-log.txt 147 | pip-delete-this-directory.txt 148 | 149 | # Unit test / coverage reports 150 | htmlcov/ 151 | .tox/ 152 | .coverage 153 | .coverage.* 154 | .cache 155 | .pytest_cache/ 156 | nosetests.xml 157 | coverage.xml 158 | *.cover 159 | .hypothesis/ 160 | 161 | # Translations 162 | *.mo 163 | *.pot 164 | 165 | # Flask stuff: 166 | instance/ 167 | .webassets-cache 168 | 169 | # Scrapy stuff: 170 | .scrapy 171 | 172 | # Sphinx documentation 173 | docs/_build/ 174 | 175 | # PyBuilder 176 | target/ 177 | 178 | # Jupyter Notebook 179 | .ipynb_checkpoints 180 | 181 | # pyenv 182 | .python-version 183 | 184 | # celery beat schedule file 185 | celerybeat-schedule.* 186 | 187 | # SageMath parsed files 188 | *.sage.py 189 | 190 | # Environments 191 | .env 192 | .venv 193 | env/ 194 | venv/ 195 | ENV/ 196 | env.bak/ 197 | venv.bak/ 198 | 199 | # Spyder project settings 200 | .spyderproject 201 | .spyproject 202 | 203 | # Rope project settings 204 | .ropeproject 205 | 206 | # mkdocs documentation 207 | /site 208 | 209 | # mypy 210 | .mypy_cache/ 211 | 212 | ### VisualStudioCode ### 213 | .vscode/* 214 | !.vscode/settings.json 215 | !.vscode/tasks.json 216 | !.vscode/launch.json 217 | !.vscode/extensions.json 218 | .history 219 | 220 | ### Windows ### 221 | # Windows thumbnail cache files 222 | Thumbs.db 223 | ehthumbs.db 224 | ehthumbs_vista.db 225 | 226 | # Folder config file 227 | Desktop.ini 228 | 229 | # Recycle Bin used on file shares 230 | $RECYCLE.BIN/ 231 | 232 | # Windows Installer files 233 | *.cab 234 | *.msi 235 | *.msm 236 | *.msp 237 | 238 | # Windows shortcuts 239 | *.lnk 240 | 241 | # Build folder 242 | 243 | */build/* 244 | 245 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 246 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.project_name }} 2 | 3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes [Lambda Powertools for operational best practices](https://github.com/awslabs/aws-lambda-powertools-python), and the following files and folders. 4 | 5 | - **`hello_world`** - Code for the application's Lambda function. 6 | - **`events`** - Invocation events that you can use to invoke the function. 7 | - **`tests`** - Unit tests for the application code. 8 | - **`template.yaml`** - A template that defines the application's AWS resources. 9 | - **`Makefile`** - Makefile for your convenience to install deps, build, invoke, and deploy your application. 10 | 11 | If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. 12 | 13 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) 14 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 15 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 16 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) 17 | 18 | ## Requirements 19 | 20 | **Make sure you have the following installed before you proceed** 21 | 22 | * AWS CLI - [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) configured with Administrator permission 23 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 24 | * [Python 3 installed](https://www.python.org/downloads/) 25 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 26 | 27 | ## Deploy the sample application 28 | 29 | > **Already know this sample? Run: `make hurry` - This command will install app deps, build, and deploy your Serverless application using SAM.** 30 | 31 | Build and deploy your application for the first time by running the following commands in your shell: 32 | 33 | ```bash 34 | {{ cookiecutter.project_name }}$ make build 35 | {{ cookiecutter.project_name }}$ make deploy.guided 36 | ``` 37 | 38 | The first command will **build** the source of your application within a Docker container. The second command will **package and deploy** your application to AWS. Guided deploy means SAM CLI will ask you about the name of your deployment/stack, AWS Region, and whether you want to save your choices, so that you can use `make deploy` next time. 39 | 40 | ## Use the SAM CLI to build and test locally 41 | 42 | Whenever you change your application code, you'll have to run build command: 43 | 44 | ```bash 45 | {{ cookiecutter.project_name }}$ make build 46 | ``` 47 | 48 | The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 49 | 50 | Test a single function by invoking it directly with a test event: 51 | 52 | ```bash 53 | {{ cookiecutter.project_name }}$ make invoke 54 | ``` 55 | 56 | > An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. 57 | 58 | The SAM CLI can also emulate your application's API. Use the `make run` to run the API locally on port 3000. 59 | 60 | ```bash 61 | {{ cookiecutter.project_name }}$ make run 62 | {{ cookiecutter.project_name }}$ curl http://localhost:3000/hello 63 | ``` 64 | 65 | The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. 66 | 67 | ```yaml 68 | Events: 69 | HelloWorld: 70 | Type: Api 71 | Properties: 72 | Path: /hello 73 | Method: get 74 | ``` 75 | 76 | ## Fetch, tail, and filter Lambda function logs 77 | 78 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. 79 | 80 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 81 | 82 | ```bash 83 | {{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name --tail 84 | ``` 85 | 86 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 87 | 88 | ## Unit tests 89 | 90 | Tests are defined in the `tests` folder in this project, and we use Pytest as the test runner for this sample project. 91 | 92 | Make sure you install dev dependencies before you run tests with `make dev`: 93 | 94 | ```bash 95 | {{ cookiecutter.project_name }}$ make dev 96 | {{ cookiecutter.project_name }}$ make test 97 | ``` 98 | 99 | ## Cleanup 100 | 101 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 102 | 103 | ```bash 104 | {{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} 105 | ``` 106 | 107 | # Appendix 108 | 109 | ## Powertools 110 | 111 | **Tracing** 112 | 113 | [Tracer utility](https://awslabs.github.io/aws-lambda-powertools-python/core/tracer/) patches known libraries, and trace the execution of this sample code including the response and exceptions as tracing metadata - You can visualize them in AWS X-Ray. 114 | 115 | **Logger** 116 | 117 | [Logger utility](https://awslabs.github.io/aws-lambda-powertools-python/core/logger/) creates an opinionated application Logger with structured logging as the output, dynamically samples 10% of your logs in DEBUG mode for concurrent invocations, log incoming events as your function is invoked, and injects key information from Lambda context object into your Logger - You can visualize them in Amazon CloudWatch Logs. 118 | 119 | **Metrics** 120 | 121 | [Metrics utility](https://awslabs.github.io/aws-lambda-powertools-python/core/metrics/) captures cold start metric of your Lambda invocation, and could add additional metrics to help you understand your application KPIs - You can visualize them in Amazon CloudWatch. 122 | 123 | ## Makefile 124 | 125 | We included a `Makefile` for your convenience - You can find all commands you can use by running `make`. Under the hood, we're using SAM CLI commands to run these common tasks: 126 | 127 | * **`make build`**: `sam build --use-container` 128 | * **`make deploy.guided`**: `sam deploy --guided` 129 | * **`make invoke`**: `sam local invoke HelloWorldFunction --event events/hello_world_event.json` 130 | * **`make run`**: `sam local start-api` 131 | 132 | ## Sync project with function dependencies 133 | 134 | Pipenv takes care of isolating dev dependencies and app dependencies. As SAM CLI requires a `requirements.txt` file, you'd need to generate one if new app dependencies have been added: 135 | 136 | ```bash 137 | {{ cookiecutter.project_name }}$ pipenv lock -r > hello_world/requirements.txt 138 | ``` 139 | -------------------------------------------------------------------------------- /{{ cookiecutter.project_name }}/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "0a309cfecf05e0ca3f3da3a81e65965f69f74ee72aef95e42fe256a6b340efb0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aws-lambda-powertools": { 20 | "extras": [ 21 | "tracer" 22 | ], 23 | "hashes": [ 24 | "sha256:3f7184ec51555e655d9f706724c473b337d1be593e57537b3fce6d75af2ba7f9", 25 | "sha256:9fe003143f2b530ded7fe4867f7b2b78b3a3401ea01042dd62f868ca66819f03" 26 | ], 27 | "index": "pypi", 28 | "markers": "python_full_version >= '3.7.4' and python_full_version < '4.0.0'", 29 | "version": "==2.0.0" 30 | }, 31 | "aws-xray-sdk": { 32 | "hashes": [ 33 | "sha256:7551e81a796e1a5471ebe84844c40e8edf7c218db33506d046fec61f7495eda4", 34 | "sha256:9b14924fd0628cf92936055864655354003f0b1acc3e1c3ffde6403d0799dd7a" 35 | ], 36 | "version": "==2.10.0" 37 | }, 38 | "botocore": { 39 | "hashes": [ 40 | "sha256:e41a81a18511f2f9181b2a9ab302a55c0effecccbef846c55aad0c47bfdbefb9", 41 | "sha256:fc0a13ef6042e890e361cf408759230f8574409bb51f81740d2e5d8ad5d1fbea" 42 | ], 43 | "markers": "python_version >= '3.7'", 44 | "version": "==1.27.96" 45 | }, 46 | "jmespath": { 47 | "hashes": [ 48 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 49 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 50 | ], 51 | "markers": "python_version >= '3.7'", 52 | "version": "==1.0.1" 53 | }, 54 | "python-dateutil": { 55 | "hashes": [ 56 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 57 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 58 | ], 59 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 60 | "version": "==2.8.2" 61 | }, 62 | "six": { 63 | "hashes": [ 64 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 65 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 66 | ], 67 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 68 | "version": "==1.16.0" 69 | }, 70 | "urllib3": { 71 | "hashes": [ 72 | "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", 73 | "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" 74 | ], 75 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", 76 | "version": "==1.26.12" 77 | }, 78 | "wrapt": { 79 | "hashes": [ 80 | "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", 81 | "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", 82 | "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", 83 | "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", 84 | "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", 85 | "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", 86 | "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", 87 | "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", 88 | "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", 89 | "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", 90 | "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", 91 | "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", 92 | "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", 93 | "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", 94 | "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", 95 | "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", 96 | "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", 97 | "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", 98 | "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", 99 | "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", 100 | "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", 101 | "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", 102 | "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", 103 | "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", 104 | "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", 105 | "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", 106 | "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", 107 | "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", 108 | "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", 109 | "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", 110 | "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", 111 | "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", 112 | "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", 113 | "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", 114 | "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", 115 | "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", 116 | "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", 117 | "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", 118 | "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", 119 | "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", 120 | "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", 121 | "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", 122 | "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", 123 | "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", 124 | "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", 125 | "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", 126 | "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", 127 | "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", 128 | "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", 129 | "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", 130 | "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", 131 | "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", 132 | "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", 133 | "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", 134 | "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", 135 | "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", 136 | "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", 137 | "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", 138 | "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", 139 | "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", 140 | "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", 141 | "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", 142 | "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", 143 | "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" 144 | ], 145 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 146 | "version": "==1.14.1" 147 | } 148 | }, 149 | "develop": { 150 | "attrs": { 151 | "hashes": [ 152 | "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", 153 | "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" 154 | ], 155 | "markers": "python_version >= '3.5'", 156 | "version": "==22.1.0" 157 | }, 158 | "boto3": { 159 | "hashes": [ 160 | "sha256:6b8899542cff82becceb3498a2240bf77c96def0515b0a31f7f6a9d5b92e7a3d", 161 | "sha256:748c055214c629744c34c7f94bfa888733dfac0b92e1daef9c243e1391ea4f53" 162 | ], 163 | "index": "pypi", 164 | "version": "==1.24.96" 165 | }, 166 | "boto3-stubs": { 167 | "hashes": [ 168 | "sha256:2a4a24d4a0fdc47d4dc2dcf1c245d378ba79b570be9a8a7dd30217ddf774ac9a", 169 | "sha256:8fd7c48fcc54c75a2579565275fee2ee763cb9f862f0edfce896104d0aa9d954" 170 | ], 171 | "index": "pypi", 172 | "version": "==1.24.96" 173 | }, 174 | "botocore": { 175 | "hashes": [ 176 | "sha256:e41a81a18511f2f9181b2a9ab302a55c0effecccbef846c55aad0c47bfdbefb9", 177 | "sha256:fc0a13ef6042e890e361cf408759230f8574409bb51f81740d2e5d8ad5d1fbea" 178 | ], 179 | "markers": "python_version >= '3.7'", 180 | "version": "==1.27.96" 181 | }, 182 | "botocore-stubs": { 183 | "hashes": [ 184 | "sha256:4a70b320244752a4dce8239329c9a9cede4a0781e924314f621ec872afe23450", 185 | "sha256:bffbb183adb645a4a6a436c24386f4c33a3f42e422d1fa1bf6b5fb30c5bc0f27" 186 | ], 187 | "markers": "python_version >= '3.7' and python_version < '4.0'", 188 | "version": "==1.27.96" 189 | }, 190 | "coverage": { 191 | "extras": [ 192 | "toml" 193 | ], 194 | "hashes": [ 195 | "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", 196 | "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", 197 | "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", 198 | "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", 199 | "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", 200 | "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", 201 | "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", 202 | "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", 203 | "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", 204 | "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", 205 | "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", 206 | "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", 207 | "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", 208 | "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", 209 | "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", 210 | "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", 211 | "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", 212 | "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", 213 | "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", 214 | "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", 215 | "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", 216 | "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", 217 | "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", 218 | "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", 219 | "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", 220 | "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", 221 | "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", 222 | "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", 223 | "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", 224 | "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", 225 | "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", 226 | "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", 227 | "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", 228 | "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", 229 | "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", 230 | "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", 231 | "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", 232 | "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", 233 | "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", 234 | "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", 235 | "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", 236 | "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", 237 | "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", 238 | "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", 239 | "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", 240 | "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", 241 | "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", 242 | "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", 243 | "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", 244 | "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" 245 | ], 246 | "markers": "python_version >= '3.7'", 247 | "version": "==6.5.0" 248 | }, 249 | "iniconfig": { 250 | "hashes": [ 251 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 252 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 253 | ], 254 | "version": "==1.1.1" 255 | }, 256 | "jmespath": { 257 | "hashes": [ 258 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 259 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 260 | ], 261 | "markers": "python_version >= '3.7'", 262 | "version": "==1.0.1" 263 | }, 264 | "packaging": { 265 | "hashes": [ 266 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 267 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 268 | ], 269 | "markers": "python_version >= '3.6'", 270 | "version": "==21.3" 271 | }, 272 | "pluggy": { 273 | "hashes": [ 274 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 275 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 276 | ], 277 | "markers": "python_version >= '3.6'", 278 | "version": "==1.0.0" 279 | }, 280 | "py": { 281 | "hashes": [ 282 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 283 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 284 | ], 285 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 286 | "version": "==1.11.0" 287 | }, 288 | "pyparsing": { 289 | "hashes": [ 290 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 291 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 292 | ], 293 | "markers": "python_full_version >= '3.6.8'", 294 | "version": "==3.0.9" 295 | }, 296 | "pytest": { 297 | "hashes": [ 298 | "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", 299 | "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" 300 | ], 301 | "index": "pypi", 302 | "version": "==7.1.3" 303 | }, 304 | "pytest-cov": { 305 | "hashes": [ 306 | "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", 307 | "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" 308 | ], 309 | "index": "pypi", 310 | "version": "==4.0.0" 311 | }, 312 | "python-dateutil": { 313 | "hashes": [ 314 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 315 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 316 | ], 317 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 318 | "version": "==2.8.2" 319 | }, 320 | "s3transfer": { 321 | "hashes": [ 322 | "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", 323 | "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" 324 | ], 325 | "markers": "python_version >= '3.7'", 326 | "version": "==0.6.0" 327 | }, 328 | "six": { 329 | "hashes": [ 330 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 331 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 332 | ], 333 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 334 | "version": "==1.16.0" 335 | }, 336 | "tomli": { 337 | "hashes": [ 338 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 339 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 340 | ], 341 | "markers": "python_version >= '3.7'", 342 | "version": "==2.0.1" 343 | }, 344 | "types-awscrt": { 345 | "hashes": [ 346 | "sha256:42a99eaa9e9312e723e4b3793e23c5be155cdacde20cb554ff0d74fd6780808b", 347 | "sha256:b65355b9926ce0cdf5f9949b2c310937f6f44b2e56292243888041dce60bc47b" 348 | ], 349 | "markers": "python_version >= '3.7' and python_version < '4.0'", 350 | "version": "==0.14.7" 351 | }, 352 | "types-s3transfer": { 353 | "hashes": [ 354 | "sha256:0c45331bfd0db210f5043efd2bc3ecd4b3482a02c28faea33edf719b6be38431", 355 | "sha256:78f9ea029fedc97e1a5d323edd149a827d4ab361d11cd92bb1b5f235ff84ba72" 356 | ], 357 | "markers": "python_version >= '3.7' and python_version < '4.0'", 358 | "version": "==0.6.0.post4" 359 | }, 360 | "typing-extensions": { 361 | "hashes": [ 362 | "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", 363 | "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" 364 | ], 365 | "markers": "python_version >= '3.7'", 366 | "version": "==4.4.0" 367 | }, 368 | "urllib3": { 369 | "hashes": [ 370 | "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", 371 | "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" 372 | ], 373 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", 374 | "version": "==1.26.12" 375 | } 376 | } 377 | } 378 | --------------------------------------------------------------------------------