Skip to content

Commit 986b497

Browse files
authored
Merge branch 'develop' into fix/1303-appsync-batch-invoke
2 parents 2714b0f + 29d6b94 commit 986b497

File tree

10 files changed

+221
-27
lines changed

10 files changed

+221
-27
lines changed

aws_lambda_powertools/utilities/parser/models/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
RequestContextV2AuthorizerJwt,
1515
RequestContextV2Http,
1616
)
17+
from .cloudformation_custom_resource import (
18+
CloudFormationCustomResourceBaseModel,
19+
CloudFormationCustomResourceCreateModel,
20+
CloudFormationCustomResourceDeleteModel,
21+
CloudFormationCustomResourceUpdateModel,
22+
)
1723
from .cloudwatch import (
1824
CloudWatchLogsData,
1925
CloudWatchLogsDecode,
@@ -147,4 +153,8 @@
147153
"KafkaBaseEventModel",
148154
"KinesisFirehoseSqsModel",
149155
"KinesisFirehoseSqsRecord",
156+
"CloudFormationCustomResourceUpdateModel",
157+
"CloudFormationCustomResourceDeleteModel",
158+
"CloudFormationCustomResourceCreateModel",
159+
"CloudFormationCustomResourceBaseModel",
150160
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any, Dict, Union
2+
3+
from pydantic import BaseModel, Field, HttpUrl
4+
5+
from aws_lambda_powertools.utilities.parser.types import Literal
6+
7+
8+
class CloudFormationCustomResourceBaseModel(BaseModel):
9+
request_type: str = Field(..., alias="RequestType")
10+
service_token: str = Field(..., alias="ServiceToken")
11+
response_url: HttpUrl = Field(..., alias="ResponseURL")
12+
stack_id: str = Field(..., alias="StackId")
13+
request_id: str = Field(..., alias="RequestId")
14+
logical_resource_id: str = Field(..., alias="LogicalResourceId")
15+
resource_type: str = Field(..., alias="ResourceType")
16+
resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="ResourceProperties")
17+
18+
19+
class CloudFormationCustomResourceCreateModel(CloudFormationCustomResourceBaseModel):
20+
request_type: Literal["Create"] = Field(..., alias="RequestType")
21+
22+
23+
class CloudFormationCustomResourceDeleteModel(CloudFormationCustomResourceBaseModel):
24+
request_type: Literal["Delete"] = Field(..., alias="RequestType")
25+
26+
27+
class CloudFormationCustomResourceUpdateModel(CloudFormationCustomResourceBaseModel):
28+
request_type: Literal["Update"] = Field(..., alias="RequestType")
29+
old_resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="OldResourceProperties")

docs/core/event_handler/api_gateway.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ For convenience, these are the default values when using `CORSConfig` to enable
327327
If you need to allow multiple origins, pass the additional origins using the `extra_origins` key.
328328

329329
| Key | Value | Note |
330-
|----------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
330+
| -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
331331
| **[allow_origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `str` | `*` | Only use the default value for development. **Never use `*` for production** unless your use case requires it |
332332
| **[extra_origins](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `List[str]` | `[]` | Additional origins to be allowed, in addition to the one specified in `allow_origin` |
333333
| **[allow_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers){target="_blank"}**: `List[str]` | `[Authorization, Content-Type, X-Amz-Date, X-Api-Key, X-Amz-Security-Token]` | Additional headers will be appended to the default list for your convenience |
@@ -367,7 +367,7 @@ You can compress with gzip and base64 encode your responses via `compress` param
367367

368368
=== "compressing_responses.py"
369369

370-
```python hl_lines="14"
370+
```python hl_lines="17 27"
371371
--8<-- "examples/event_handler_rest/src/compressing_responses.py"
372372
```
373373

@@ -486,7 +486,7 @@ When necessary, you can set a prefix when including a router object. This means
486486
You can use specialized router classes according to the type of event that you are resolving. This way you'll get type hints from your IDE as you access the `current_event` property.
487487

488488
| Router | Resolver | `current_event` type |
489-
|-------------------------|---------------------------|------------------------|
489+
| ----------------------- | ------------------------- | ---------------------- |
490490
| APIGatewayRouter | APIGatewayRestResolver | APIGatewayProxyEvent |
491491
| APIGatewayHttpRouter | APIGatewayHttpResolver | APIGatewayProxyEventV2 |
492492
| ALBRouter | ALBResolver | ALBEvent |

docs/utilities/parser.md

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -156,27 +156,30 @@ def my_function():
156156

157157
Parser comes with the following built-in models:
158158

159-
| Model name | Description |
160-
| --------------------------------------- | ------------------------------------------------------------------------------------- |
161-
| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer |
162-
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
163-
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
164-
| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs |
165-
| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams |
166-
| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge |
167-
| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload |
168-
| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload |
169-
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
170-
| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose |
171-
| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
172-
| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |
173-
| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
174-
| **S3Model** | Lambda Event Source payload for Amazon S3 |
175-
| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
176-
| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
177-
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
178-
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
179-
| **SqsModel** | Lambda Event Source payload for Amazon SQS |
159+
| Model name | Description |
160+
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
161+
| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer |
162+
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
163+
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
164+
| **CloudFormationCustomResourceCreateModel** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation |
165+
| **CloudFormationCustomResourceUpdateModel** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation |
166+
| **CloudFormationCustomResourceDeleteModel** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation |
167+
| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs |
168+
| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams |
169+
| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge |
170+
| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload |
171+
| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload |
172+
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
173+
| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose |
174+
| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
175+
| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |
176+
| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
177+
| **S3Model** | Lambda Event Source payload for Amazon S3 |
178+
| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
179+
| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
180+
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
181+
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
182+
| **SqsModel** | Lambda Event Source payload for Amazon SQS |
180183

181184
#### Extending built-in models
182185

docs/we_made_this.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ This article walks through how CyberArk uses Powertools to implement Feature Fla
7676

7777
* [aws.amazon.com/blogs/mt/how-cyberark-implements-feature-flags-with-aws-appconfig](https://aws.amazon.com/blogs/mt/how-cyberark-implements-feature-flags-with-aws-appconfig){target="_blank"}
7878

79+
### Designing for Idempotency
80+
81+
> **Author: [Valentin Dreismann](linkedin.com/in/valentin-dreismann-69694b16a){target="_blank"}** :material-linkedin:
82+
83+
This article outlines the importance of idempotency, key considerations and trade-offs when implementing in your systems.
84+
85+
* [Idempotency the right way](https://engineering.cloudflight.io/idempotency-the-right-way){target="_blank"}
86+
7987
## Videos
8088

8189
#### Building a resilient input handling with Parser

examples/event_handler_rest/src/compressing_responses.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import requests
2-
from requests import Response
32

43
from aws_lambda_powertools import Logger, Tracer
5-
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
4+
from aws_lambda_powertools.event_handler import (
5+
APIGatewayRestResolver,
6+
Response,
7+
content_types,
8+
)
69
from aws_lambda_powertools.logging import correlation_paths
710
from aws_lambda_powertools.utilities.typing import LambdaContext
811

@@ -14,13 +17,22 @@
1417
@app.get("/todos", compress=True)
1518
@tracer.capture_method
1619
def get_todos():
17-
todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
20+
todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos")
1821
todos.raise_for_status()
1922

2023
# for brevity, we'll limit to the first 10 only
2124
return {"todos": todos.json()[:10]}
2225

2326

27+
@app.get("/todos/<todo_id>", compress=True)
28+
@tracer.capture_method
29+
def get_todo_by_id(todo_id: str): # same example using Response class
30+
todos: requests.Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
31+
todos.raise_for_status()
32+
33+
return Response(status_code=200, content_type=content_types.APPLICATION_JSON, body=todos.json())
34+
35+
2436
# You can continue to use other utilities just as before
2537
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
2638
@tracer.capture_lambda_handler
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"RequestType": "Create",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "ss"
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"RequestType": "Delete",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "ss"
12+
}
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"RequestType": "Update",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "new"
12+
},
13+
"OldResourceProperties": {
14+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx-xxxx-xxx",
15+
"MyProps": "old"
16+
}
17+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import pytest
2+
from pydantic import BaseModel, Field
3+
4+
from aws_lambda_powertools.utilities.parser import ValidationError
5+
from aws_lambda_powertools.utilities.parser.models import (
6+
CloudFormationCustomResourceCreateModel,
7+
CloudFormationCustomResourceDeleteModel,
8+
CloudFormationCustomResourceUpdateModel,
9+
)
10+
from tests.functional.utils import load_event
11+
12+
13+
def test_cloudformation_custom_resource_create_event():
14+
raw_event = load_event("cloudformationCustomResourceCreate.json")
15+
model = CloudFormationCustomResourceCreateModel(**raw_event)
16+
17+
assert model.request_type == raw_event["RequestType"]
18+
assert model.request_id == raw_event["RequestId"]
19+
assert model.service_token == raw_event["ServiceToken"]
20+
assert str(model.response_url) == raw_event["ResponseURL"]
21+
assert model.stack_id == raw_event["StackId"]
22+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
23+
assert model.resource_type == raw_event["ResourceType"]
24+
assert model.resource_properties == raw_event["ResourceProperties"]
25+
26+
27+
def test_cloudformation_custom_resource_create_event_custom_model():
28+
class MyModel(BaseModel):
29+
MyProps: str
30+
31+
class MyCustomResource(CloudFormationCustomResourceCreateModel):
32+
resource_properties: MyModel = Field(..., alias="ResourceProperties")
33+
34+
raw_event = load_event("cloudformationCustomResourceCreate.json")
35+
model = MyCustomResource(**raw_event)
36+
37+
assert model.resource_properties.MyProps == raw_event["ResourceProperties"].get("MyProps")
38+
39+
40+
def test_cloudformation_custom_resource_create_event_invalid():
41+
raw_event = load_event("cloudformationCustomResourceCreate.json")
42+
raw_event["ResourceProperties"] = ["some_data"]
43+
44+
with pytest.raises(ValidationError):
45+
CloudFormationCustomResourceCreateModel(**raw_event)
46+
47+
48+
def test_cloudformation_custom_resource_update_event():
49+
raw_event = load_event("cloudformationCustomResourceUpdate.json")
50+
model = CloudFormationCustomResourceUpdateModel(**raw_event)
51+
52+
assert model.request_type == raw_event["RequestType"]
53+
assert model.request_id == raw_event["RequestId"]
54+
assert model.service_token == raw_event["ServiceToken"]
55+
assert str(model.response_url) == raw_event["ResponseURL"]
56+
assert model.stack_id == raw_event["StackId"]
57+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
58+
assert model.resource_type == raw_event["ResourceType"]
59+
assert model.resource_properties == raw_event["ResourceProperties"]
60+
assert model.old_resource_properties == raw_event["OldResourceProperties"]
61+
62+
63+
def test_cloudformation_custom_resource_update_event_invalid():
64+
raw_event = load_event("cloudformationCustomResourceUpdate.json")
65+
raw_event["OldResourceProperties"] = ["some_data"]
66+
67+
with pytest.raises(ValidationError):
68+
CloudFormationCustomResourceUpdateModel(**raw_event)
69+
70+
71+
def test_cloudformation_custom_resource_delete_event():
72+
raw_event = load_event("cloudformationCustomResourceDelete.json")
73+
model = CloudFormationCustomResourceDeleteModel(**raw_event)
74+
75+
assert model.request_type == raw_event["RequestType"]
76+
assert model.request_id == raw_event["RequestId"]
77+
assert model.service_token == raw_event["ServiceToken"]
78+
assert str(model.response_url) == raw_event["ResponseURL"]
79+
assert model.stack_id == raw_event["StackId"]
80+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
81+
assert model.resource_type == raw_event["ResourceType"]
82+
assert model.resource_properties == raw_event["ResourceProperties"]
83+
84+
85+
def test_cloudformation_custom_resource_delete_event_invalid():
86+
raw_event = load_event("cloudformationCustomResourceDelete.json")
87+
raw_event["ResourceProperties"] = ["some_data"]
88+
with pytest.raises(ValidationError):
89+
CloudFormationCustomResourceDeleteModel(**raw_event)

0 commit comments

Comments
 (0)