Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aws-powertools/powertools-lambda-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.21.1
Choose a base ref
...
head repository: aws-powertools/powertools-lambda-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.22.0
Choose a head ref
Loading
Showing with 1,691 additions and 321 deletions.
  1. +21 −2 .github/workflows/publish.yml
  2. +56 −0 CHANGELOG.md
  3. +129 −69 aws_lambda_powertools/event_handler/api_gateway.py
  4. +48 −27 aws_lambda_powertools/event_handler/appsync.py
  5. +125 −0 aws_lambda_powertools/utilities/data_classes/active_mq_event.py
  6. +121 −0 aws_lambda_powertools/utilities/data_classes/rabbit_mq_event.py
  7. +23 −5 aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py
  8. +1 −1 aws_lambda_powertools/utilities/parser/models/apigw.py
  9. +2 −2 aws_lambda_powertools/utilities/parser/models/apigwv2.py
  10. +2 −2 aws_lambda_powertools/utilities/validation/exceptions.py
  11. +330 −6 docs/core/event_handler/api_gateway.md
  12. +60 −0 docs/core/event_handler/appsync.md
  13. +111 −71 docs/index.md
  14. BIN docs/media/micro-function.png
  15. BIN docs/media/monolithic-function.png
  16. +54 −0 docs/utilities/data_classes.md
  17. +62 −6 docs/utilities/idempotency.md
  18. +1 −2 docs/utilities/middleware_factory.md
  19. +4 −2 docs/utilities/parser.md
  20. +2 −0 mkdocs.yml
  21. +147 −109 poetry.lock
  22. +10 −10 pyproject.toml
  23. +45 −0 tests/events/activeMQEvent.json
  24. +51 −0 tests/events/rabbitMQEvent.json
  25. +69 −0 tests/functional/data_classes/test_amazon_mq.py
  26. +161 −0 tests/functional/event_handler/test_api_gateway.py
  27. +27 −0 tests/functional/event_handler/test_appsync.py
  28. +3 −3 tests/functional/idempotency/test_idempotency.py
  29. +7 −1 tests/functional/parser/test_apigw.py
  30. +14 −1 tests/functional/parser/test_apigwv2.py
  31. +5 −2 tests/functional/test_logger.py
23 changes: 21 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -30,6 +30,14 @@ name: Publish to PyPi
# 2. Use the version released under Releases e.g. v1.13.0
#

#
# === Documentation hotfix ===
#
# 1. Trigger "Publish to PyPi" workflow manually: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow
# 2. Use the latest version released under Releases e.g. v1.21.1
# 3. Set `Build and publish docs only` field to `true`


on:
release:
types: [published]
@@ -38,6 +46,10 @@ on:
publish_version:
description: 'Version to publish, e.g. v1.13.0'
required: true
publish_docs_only:
description: 'Build and publish docs only'
required: false
default: 'false'

jobs:
release:
@@ -54,29 +66,35 @@ jobs:
run: |
RELEASE_TAG_VERSION=${{ github.event.release.tag_name }}
# Replace publishing version if the workflow was triggered manually
test -n $RELEASE_TAG_VERSION && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }}
# test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }}
echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV
- name: Ensure new version is also set in pyproject and CHANGELOG
if: ${{ github.event.inputs.publish_docs_only == false }}
run: |
grep --regexp "${RELEASE_TAG_VERSION}" CHANGELOG.md
grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml
- name: Install dependencies
run: make dev
- name: Run all tests, linting and baselines
if: ${{ github.event.inputs.publish_docs_only == false }}
run: make pr
- name: Build python package and wheel
if: ${{ github.event.inputs.publish_docs_only == false }}
run: poetry build
- name: Upload to PyPi test
if: ${{ github.event.inputs.publish_docs_only == false }}
run: make release-test
env:
PYPI_USERNAME: __token__
PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }}
- name: Upload to PyPi prod
if: ${{ github.event.inputs.publish_docs_only == false }}
run: make release-prod
env:
PYPI_USERNAME: __token__
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- name: publish lambda layer in SAR by triggering the internal codepipeline
if: ${{ github.event.inputs.publish_docs_only == false }}
run: |
aws ssm put-parameter --name "powertools-python-release-version" --value $RELEASE_TAG_VERSION --overwrite
aws codepipeline start-pipeline-execution --name ${{ secrets.CODEPIPELINE_NAME }}
@@ -88,7 +106,7 @@ jobs:
- name: Setup doc deploy
run: |
git config --global user.name Docs deploy
git config --global user.email docs@dummy.bot.com
git config --global user.email aws-devax-open-source@amazon.com
- name: Build docs website and API reference
run: |
make release-docs VERSION=${RELEASE_TAG_VERSION} ALIAS="latest"
@@ -111,6 +129,7 @@ jobs:
sync_master:
needs: release
runs-on: ubuntu-latest
if: ${{ github.event.inputs.publish_docs_only == false }}
steps:
- uses: actions/checkout@v2
- name: Sync master from detached head
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,62 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo

## [Unreleased]

## 1.22.0 - 2021-11-17

Tenet update! We've updated **Idiomatic** tenet to **Progressive** to reflect the new Router feature in Event Handler, and more importantly the new wave of customers coming from SRE, Data Analysis, and Data Science background.

* BEFORE: **Idiomatic**. Utilities follow programming language idioms and language-specific best practices.
* AFTER: **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices.
### Bug Fixes

* **ci:** change supported python version from 3.6.1 to 3.6.2, bump black ([#807](https://github.com/awslabs/aws-lambda-powertools-python/issues/807))
* **ci:** skip sync master on docs hotfix
* **parser:** body and query strings can be null or omitted in ApiGatewayProxyEventModel and ApiGatewayProxyEventV2Model ([#820](https://github.com/awslabs/aws-lambda-powertools-python/issues/820))

### Code Refactoring

* **apigateway:** Add BaseRouter and duplicate route check ([#757](https://github.com/awslabs/aws-lambda-powertools-python/issues/757))

### Documentation

* **docs:** updated Lambda Layers definition & limitations. ([#775](https://github.com/awslabs/aws-lambda-powertools-python/issues/775))
* **docs:** Idiomatic tenet updated to Progressive
* **docs:** use higher contrast font to improve accessibility ([#822](https://github.com/awslabs/aws-lambda-powertools-python/issues/822))
* **docs:** fix indentation of SAM snippets in install section ([#778](https://github.com/awslabs/aws-lambda-powertools-python/issues/778))
* **docs:** improve public lambda layer wording, add clipboard buttons to improve UX ([#762](https://github.com/awslabs/aws-lambda-powertools-python/issues/762))
* **docs:** add amplify-cli instructions for public layer ([#754](https://github.com/awslabs/aws-lambda-powertools-python/issues/754))
* **api-gateway:** add new router feature to allow route splitting in API Gateway and ALB ([#767](https://github.com/awslabs/aws-lambda-powertools-python/issues/767))
* **apigateway:** re-add sample layout, add considerations ([#826](https://github.com/awslabs/aws-lambda-powertools-python/issues/826))
* **appsync:** add new router feature to allow GraphQL Resolver composition ([#821](https://github.com/awslabs/aws-lambda-powertools-python/issues/821))
* **idempotency:** add support for DynamoDB composite keys ([#808](https://github.com/awslabs/aws-lambda-powertools-python/issues/808))
* **tenets:** update Idiomatic tenet to Progressive ([#823](https://github.com/awslabs/aws-lambda-powertools-python/issues/823))
* **docs:** remove Lambda Layer version tag
### Features

* **apigateway:** add Router to allow large routing composition ([#645](https://github.com/awslabs/aws-lambda-powertools-python/issues/645))
* **appsync:** add Router to allow large resolver composition ([#776](https://github.com/awslabs/aws-lambda-powertools-python/issues/776))
* **data-classes:** ActiveMQ and RabbitMQ support ([#770](https://github.com/awslabs/aws-lambda-powertools-python/issues/770))
* **logger:** add ALB correlation ID support ([#816](https://github.com/awslabs/aws-lambda-powertools-python/issues/816))

### Maintenance

* **deps:** bump boto3 from 1.19.6 to 1.20.3 ([#809](https://github.com/awslabs/aws-lambda-powertools-python/issues/809))
* **deps:** bump boto3 from 1.18.58 to 1.18.59 ([#760](https://github.com/awslabs/aws-lambda-powertools-python/issues/760))
* **deps:** bump urllib3 from 1.26.4 to 1.26.5 ([#787](https://github.com/awslabs/aws-lambda-powertools-python/issues/787))
* **deps:** bump boto3 from 1.18.61 to 1.19.6 ([#783](https://github.com/awslabs/aws-lambda-powertools-python/issues/783))
* **deps:** bump boto3 from 1.18.56 to 1.18.58 ([#755](https://github.com/awslabs/aws-lambda-powertools-python/issues/755))
* **deps:** bump boto3 from 1.18.59 to 1.18.61 ([#766](https://github.com/awslabs/aws-lambda-powertools-python/issues/766))
* **deps:** bump boto3 from 1.20.3 to 1.20.5 ([#817](https://github.com/awslabs/aws-lambda-powertools-python/issues/817))
* **deps-dev:** bump coverage from 6.0.1 to 6.0.2 ([#764](https://github.com/awslabs/aws-lambda-powertools-python/issues/764))
* **deps-dev:** bump pytest-asyncio from 0.15.1 to 0.16.0 ([#782](https://github.com/awslabs/aws-lambda-powertools-python/issues/782))
* **deps-dev:** bump flake8-eradicate from 1.1.0 to 1.2.0 ([#784](https://github.com/awslabs/aws-lambda-powertools-python/issues/784))
* **deps-dev:** bump flake8-comprehensions from 3.6.1 to 3.7.0 ([#759](https://github.com/awslabs/aws-lambda-powertools-python/issues/759))
* **deps-dev:** bump flake8-isort from 4.0.0 to 4.1.1 ([#785](https://github.com/awslabs/aws-lambda-powertools-python/issues/785))
* **deps-dev:** bump coverage from 6.0 to 6.0.1 ([#751](https://github.com/awslabs/aws-lambda-powertools-python/issues/751))
* **deps-dev:** bump mkdocs-material from 7.3.3 to 7.3.5 ([#781](https://github.com/awslabs/aws-lambda-powertools-python/issues/781))
* **deps-dev:** bump mkdocs-material from 7.3.5 to 7.3.6 ([#791](https://github.com/awslabs/aws-lambda-powertools-python/issues/791))
* **deps-dev:** bump mkdocs-material from 7.3.2 to 7.3.3 ([#758](https://github.com/awslabs/aws-lambda-powertools-python/issues/758))

## 1.21.1 - 2021-10-07

### Regression
198 changes: 129 additions & 69 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,9 @@
import os
import re
import traceback
import warnings
import zlib
from abc import ABC, abstractmethod
from enum import Enum
from functools import partial
from http import HTTPStatus
@@ -227,78 +229,20 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic
}


class ApiGatewayResolver:
"""API Gateway and ALB proxy resolver
Examples
--------
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
```python
from aws_lambda_powertools import Tracer
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
tracer = Tracer()
app = ApiGatewayResolver()
@app.get("/get-call")
def simple_get():
return {"message": "Foo"}
@app.post("/post-call")
def simple_post():
post_data: dict = app.current_event.json_body
return {"message": post_data["value"]}
@tracer.capture_lambda_handler
def lambda_handler(event, context):
return app.resolve(event, context)
```
"""

class BaseRouter(ABC):
current_event: BaseProxyEvent
lambda_context: LambdaContext

def __init__(
@abstractmethod
def route(
self,
proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent,
cors: Optional[CORSConfig] = None,
debug: Optional[bool] = None,
serializer: Optional[Callable[[Dict], str]] = None,
strip_prefixes: Optional[List[str]] = None,
rule: str,
method: Any,
cors: Optional[bool] = None,
compress: bool = False,
cache_control: Optional[str] = None,
):
"""
Parameters
----------
proxy_type: ProxyEventType
Proxy request type, defaults to API Gateway V1
cors: CORSConfig
Optionally configure and enabled CORS. Not each route will need to have to cors=True
debug: Optional[bool]
Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG"
environment variable
serializer : Callable, optional
function to serialize `obj` to a JSON formatted `str`, by default json.dumps
strip_prefixes: List[str], optional
optional list of prefixes to be removed from the request path before doing the routing. This is often used
with api gateways with multiple custom mappings.
"""
self._proxy_type = proxy_type
self._routes: List[Route] = []
self._cors = cors
self._cors_enabled: bool = cors is not None
self._cors_methods: Set[str] = {"OPTIONS"}
self._debug = resolve_truthy_env_var_choice(
env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug
)
self._strip_prefixes = strip_prefixes

# Allow for a custom serializer or a concise json serialization
self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder)

if self._debug:
# Always does a pretty print when in debug mode
self._serializer = partial(json.dumps, indent=4, cls=Encoder)
raise NotImplementedError()

def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None):
"""Get route decorator with GET `method`
@@ -434,6 +378,78 @@ def lambda_handler(event, context):
"""
return self.route(rule, "PATCH", cors, compress, cache_control)


class ApiGatewayResolver(BaseRouter):
"""API Gateway and ALB proxy resolver
Examples
--------
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
```python
from aws_lambda_powertools import Tracer
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
tracer = Tracer()
app = ApiGatewayResolver()
@app.get("/get-call")
def simple_get():
return {"message": "Foo"}
@app.post("/post-call")
def simple_post():
post_data: dict = app.current_event.json_body
return {"message": post_data["value"]}
@tracer.capture_lambda_handler
def lambda_handler(event, context):
return app.resolve(event, context)
```
"""

def __init__(
self,
proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent,
cors: Optional[CORSConfig] = None,
debug: Optional[bool] = None,
serializer: Optional[Callable[[Dict], str]] = None,
strip_prefixes: Optional[List[str]] = None,
):
"""
Parameters
----------
proxy_type: ProxyEventType
Proxy request type, defaults to API Gateway V1
cors: CORSConfig
Optionally configure and enabled CORS. Not each route will need to have to cors=True
debug: Optional[bool]
Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG"
environment variable
serializer : Callable, optional
function to serialize `obj` to a JSON formatted `str`, by default json.dumps
strip_prefixes: List[str], optional
optional list of prefixes to be removed from the request path before doing the routing. This is often used
with api gateways with multiple custom mappings.
"""
self._proxy_type = proxy_type
self._routes: List[Route] = []
self._route_keys: List[str] = []
self._cors = cors
self._cors_enabled: bool = cors is not None
self._cors_methods: Set[str] = {"OPTIONS"}
self._debug = resolve_truthy_env_var_choice(
env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug
)
self._strip_prefixes = strip_prefixes

# Allow for a custom serializer or a concise json serialization
self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder)

if self._debug:
# Always does a pretty print when in debug mode
self._serializer = partial(json.dumps, indent=4, cls=Encoder)

def route(
self,
rule: str,
@@ -451,6 +467,10 @@ def register_resolver(func: Callable):
else:
cors_enabled = cors
self._routes.append(Route(method, self._compile_regex(rule), func, cors_enabled, compress, cache_control))
route_key = method + rule
if route_key in self._route_keys:
warnings.warn(f"A route like this was already registered. method: '{method}' rule: '{rule}'")
self._route_keys.append(route_key)
if cors_enabled:
logger.debug(f"Registering method {method.upper()} to Allow Methods in CORS")
self._cors_methods.add(method.upper())
@@ -474,8 +494,8 @@ def resolve(self, event, context) -> Dict[str, Any]:
"""
if self._debug:
print(self._json_dump(event))
self.current_event = self._to_proxy_event(event)
self.lambda_context = context
BaseRouter.current_event = self._to_proxy_event(event)
BaseRouter.lambda_context = context
return self._resolve().build(self.current_event, self._cors)

def __call__(self, event, context) -> Any:
@@ -630,3 +650,43 @@ def _to_response(self, result: Union[Dict, Response]) -> Response:

def _json_dump(self, obj: Any) -> str:
return self._serializer(obj)

def include_router(self, router: "Router", prefix: Optional[str] = None) -> None:
"""Adds all routes defined in a router
Parameters
----------
router : Router
The Router containing a list of routes to be registered after the existing routes
prefix : str, optional
An optional prefix to be added to the originally defined rule
"""
for route, func in router._routes.items():
if prefix:
rule = route[0]
rule = prefix if rule == "/" else f"{prefix}{rule}"
route = (rule, *route[1:])

self.route(*route)(func)


class Router(BaseRouter):
"""Router helper class to allow splitting ApiGatewayResolver into multiple files"""

def __init__(self):
self._routes: Dict[tuple, Callable] = {}

def route(
self,
rule: str,
method: Union[str, List[str]],
cors: Optional[bool] = None,
compress: bool = False,
cache_control: Optional[str] = None,
):
def register_route(func: Callable):
methods = method if isinstance(method, list) else [method]
for item in methods:
self._routes[(rule, item, cors, compress, cache_control)] = func

return register_route
Loading