Summary
This release highlights 1/ support for Python 3.9, 2/ support for API Gateway and AppSync Lambda Authorizers, 3/ support for API Gateway Custom Domain Mappings, 4/ support to make any Python synchronous function idempotent, and a number of documentation improvements & bugfixes.
Lambda Authorizer support
AppSync
This release adds Data Class support for AppSyncAuthorizerEvent
, AppSyncAuthorizerResponse
, and correlation ID in Logger.
You can use AppSyncAuthorizerEvent
to easily access all self-documented properties, and AppSyncAuthorizerResponse
to serialize the response in the expected format.
You can read more in the announcement blog post for more details
from typing import Dict
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.logging.logger import Logger
from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import (
AppSyncAuthorizerEvent,
AppSyncAuthorizerResponse,
)
from aws_lambda_powertools.utilities.data_classes.event_source import event_source
logger = Logger()
def get_user_by_token(token: str):
"""Look a user by token"""
@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_AUTHORIZER)
@event_source(data_class=AppSyncAuthorizerEvent)
def lambda_handler(event: AppSyncAuthorizerEvent, context) -> Dict:
user = get_user_by_token(event.authorization_token)
if not user:
# No user found, return not authorized
return AppSyncAuthorizerResponse().to_dict()
return AppSyncAuthorizerResponse(
authorize=True,
resolver_context={"id": user.id},
# Only allow admins to delete events
deny_fields=None if user.is_admin else ["Mutation.deleteEvent"],
).asdict()
API Gateway
This release adds support for both Lambda Authorizer for payload v1 - APIGatewayAuthorizerRequestEvent
, APIGatewayAuthorizerResponse
- and v2 formats APIGatewayAuthorizerEventV2
, APIGatewayAuthorizerResponseV2
.
Similar to AppSync, you can use APIGatewayAuthorizerRequestEvent
and APIGatewayAuthorizerEventV2
to easily access all self-documented properties available, and its corresponding APIGatewayAuthorizerResponse
and APIGatewayAuthorizerResponseV2
to serialize the response in the expected format.
You can read more in the announcement blog post for more details
v2 format
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
APIGatewayAuthorizerEventV2,
APIGatewayAuthorizerResponseV2,
)
from secrets import compare_digest
def get_user_by_token(token):
if compare_digest(token, "Foo"):
return {"name": "Foo"}
return None
@event_source(data_class=APIGatewayAuthorizerEventV2)
def handler(event: APIGatewayAuthorizerEventV2, context):
user = get_user_by_token(event.get_header_value("x-token"))
if user is None:
# No user was found, so we return not authorized
return APIGatewayAuthorizerResponseV2().asdict()
# Found the user and setting the details in the context
return APIGatewayAuthorizerResponseV2(authorize=True, context=user).asdict()
v1 format
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
APIGatewayAuthorizerRequestEvent,
APIGatewayAuthorizerResponse,
HttpVerb,
)
from secrets import compare_digest
def get_user_by_token(token):
if compare_digest(token, "admin-foo"):
return {"isAdmin": True, "name": "Admin"}
elif compare_digest(token, "regular-foo"):
return {"name": "Joe"}
else:
return None
@event_source(data_class=APIGatewayAuthorizerRequestEvent)
def handler(event: APIGatewayAuthorizerRequestEvent, context):
user = get_user_by_token(event.get_header_value("Authorization"))
# parse the `methodArn` as an `APIGatewayRouteArn`
arn = event.parsed_arn
# Create the response builder from parts of the `methodArn`
policy = APIGatewayAuthorizerResponse(
principal_id="user",
region=arn.region,
aws_account_id=arn.aws_account_id,
api_id=arn.api_id,
stage=arn.stage
)
if user is None:
# No user was found, so we return not authorized
policy.deny_all_routes()
return policy.asdict()
# Found the user and setting the details in the context
policy.context = user
# Conditional IAM Policy
if user.get("isAdmin", False):
policy.allow_all_routes()
else:
policy.allow_route(HttpVerb.GET, "/user-profile")
return policy.asdict()
Custom Domain API Mappings
When using Custom Domain API Mappings feature, you must use the new strip_prefixes
param in the ApiGatewayResolver
constructor.
Scenario: You have a custom domain api.mydomain.dev
and set an API Mapping payment
to forward requests to your Payments API, the path argument will be /payment/<your_actual_path>
.
This will lead to a HTTP 404 despite having your Lambda configured correctly. See the example below on how to account for this change.
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
tracer = Tracer()
logger = Logger()
app = ApiGatewayResolver(strip_prefixes=["/payment"])
@app.get("/subscriptions/<subscription>")
@tracer.capture_method
def get_subscription(subscription):
return {"subscription_id": subscription}
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
return app.resolve(event, context)
Make any Python function idempotent
Previously, you could only make the entire Lambda function handler idempotent. You can now make any Python function idempotent with the new idempotent_function
.
This also enables easy integration with any other utility in Powertools. Take example the Batch utility, where you wouldn't want to make the entire Lambda handler idempotent as the batch will vary, instead you'd want to make sure you can process a given message only once.
As a trade-off to allow any Python function with an arbitrary number of parameters, you must call your function with a keyword argument, and you tell us upfront which one that might be using data_keyword_argument
, so we can apply all operations like hashing the idempotency token, payload extraction, parameter validation, etc.
import uuid
from aws_lambda_powertools.utilities.batch import sqs_batch_processor
from aws_lambda_powertools.utilities.idempotency import idempotent_function, DynamoDBPersistenceLayer, IdempotencyConfig
dynamodb = DynamoDBPersistenceLayer(table_name="idem")
config = IdempotencyConfig(
event_key_jmespath="messageId", # see "Choosing a payload subset for idempotency" docs section
use_local_cache=True,
)
@idempotent_function(data_keyword_argument="data", config=config, persistence_store=dynamodb)
def dummy(arg_one, arg_two, data: dict, **kwargs):
return {"data": data}
@idempotent_function(data_keyword_argument="record", config=config, persistence_store=dynamodb)
def record_handler(record):
return {"message": record["body"]}
@sqs_batch_processor(record_handler=record_handler)
def lambda_handler(event, context):
# `data` parameter must be called as a keyword argument to work
dummy("hello", "universe", data="test")
return {"statusCode": 200}
Changes
πNew features and non-breaking changes
- feat(data-classes): authorizer for http api and rest api (#620) by @michaelbrewer
- feat(data-classes): data_as_bytes prop KinesisStreamRecordPayload (#628) by @hjurong
- feat(general): support for Python 3.9 (#626) by @heitorlessa
- feat(event-handler): prefixes to strip for custom mappings (#579) by @michaelbrewer
- feat(data-classes): AppSync Lambda authorizer event (#610) by @michaelbrewer
- feat(idempotency): support for any synchronous function (#625) by @heitorlessa
π Documentation updates
- docs(data-classes): make authorizer concise; use enum (#630) by @heitorlessa
- feat(data-classes): authorizer for http api and rest api (#620) by @michaelbrewer
- feat(data-classes): AppSync Lambda authorizer event (#610) by @michaelbrewer
- chore(docs): correct markdown based on markdown lint (#603) by @michaelbrewer
- docs(feature-flags): correct link and json examples (#605) by @michaelbrewer
π Bug and hot fixes
- fix(api-gateway): strip stage name from request path (#622) by @michaelbrewer
π§ Maintenance
- feat(idempotency): support for any synchronous function (#625) by @heitorlessa
- chore(deps): bump boto3 from 1.18.25 to 1.18.26 (#627) by @dependabot
- feat(general): support for Python 3.9 (#626) by @heitorlessa
- chore(deps): bump boto3 from 1.18.24 to 1.18.25 (#623) by @dependabot
- chore(api-docs): enable allow_reuse to fix the docs (#612) by @michaelbrewer
- chore(deps): bump boto3 from 1.18.22 to 1.18.24 (#619) by @dependabot
- refactor(event_handler): match to match_results; 3.10 new keyword (#616) by @michaelbrewer
- chore(deps-dev): bump flake8-comprehensions from 3.6.0 to 3.6.1 (#615) by @dependabot
- chore(deps): bump boto3 from 1.18.21 to 1.18.22 (#614) by @dependabot
- chore(shared): fix cyclic import & refactor data extraction fn (#613) by @heitorlessa
- chore(deps-dev): bump flake8-comprehensions from 3.5.0 to 3.6.0 (#609) by @dependabot
- chore(deps): bump boto3 from 1.18.17 to 1.18.21 (#608) by @dependabot
- chore(deps-dev): bump mkdocs-material from 7.2.3 to 7.2.4 (#607) by @dependabot
This release was made possible by the following contributors:
@dependabot, @dependabot[bot], @heitorlessa, @hjurong and @michaelbrewer