Skip to content

Commit d1996a6

Browse files
committed
Merge branch 'develop' into chore/refactor-e2e
* develop: chore(ci): update changelog with latest changes docs(parser): minor grammar fix (aws-powertools#1427) chore(ci): update changelog with latest changes docs(apigateway): removes duplicate admonition (aws-powertools#1426) chore(ci): update changelog with latest changes docs(jmespath_util): snippets split, improved, and lint (aws-powertools#1419) chore(ci): reduce payload and only send prod notification
2 parents 0280d9f + 6380b63 commit d1996a6

23 files changed

+608
-175
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
## Bug Fixes
88

9+
* **ci:** move from pip-tools to poetry on layers to fix conflicts
910
* **ci:** typo and bust gh actions cache
1011
* **ci:** use poetry to resolve layer deps; pip for CDK
1112
* **ci:** disable poetry venv for layer workflow as cdk ignores venv
@@ -17,12 +18,20 @@
1718

1819
## Documentation
1920

21+
* **apigateway:** removes duplicate admonition ([#1426](https://github.com/awslabs/aws-lambda-powertools-python/issues/1426))
22+
* **jmespath_util:** snippets split, improved, and lint ([#1419](https://github.com/awslabs/aws-lambda-powertools-python/issues/1419))
2023
* **layer:** upgrade to 1.27.0
24+
* **layer:** upgrade to 1.27.0
25+
* **parser:** minor grammar fix ([#1427](https://github.com/awslabs/aws-lambda-powertools-python/issues/1427))
2126

2227
## Maintenance
2328

2429
* **ci:** update changelog with latest changes
2530
* **ci:** update changelog with latest changes
31+
* **ci:** reduce payload and only send prod notification
32+
* **ci:** update changelog with latest changes
33+
* **ci:** update changelog with latest changes
34+
* **ci:** update changelog with latest changes
2635

2736

2837
<a name="v1.27.0"></a>

docs/core/event_handler/api_gateway.md

-3
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ Each dynamic route you set must be part of your function signature. This allows
135135
???+ tip
136136
You can also nest dynamic paths, for example `/todos/<todo_id>/<todo_status>`.
137137

138-
???+ tip
139-
You can also nest dynamic paths, for example `/todos/<todo_id>/<todo_status>`.
140-
141138
#### Catch-all routes
142139

143140
???+ note

docs/utilities/jmespath_functions.md

+91-163
Large diffs are not rendered by default.

docs/utilities/parser.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ Parser is best suited for those looking for a trade-off between defining their m
524524

525525
We export most common classes, exceptions, and utilities from Pydantic as part of parser e.g. `from aws_lambda_powertools.utilities.parser import BaseModel`.
526526

527-
If what's your trying to use isn't available as part of the high level import system, use the following escape hatch mechanism:
527+
If what you're trying to use isn't available as part of the high level import system, use the following escape hatch mechanism:
528528

529529
```python title="Pydantic import escape hatch"
530530
from aws_lambda_powertools.utilities.parser.pydantic import <what you'd like to import'>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"Records": [
3+
{
4+
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
5+
"receiptHandle": "MessageReceiptHandle",
6+
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\",\"booking\":{\"id\":\"5b2c4803-330b-42b7-811a-c68689425de1\",\"reference\":\"ySz7oA\",\"outboundFlightId\":\"20c0d2f2-56a3-4068-bf20-ff7703db552d\"},\"payment\":{\"receipt\":\"https:\/\/pay.stripe.com\/receipts\/acct_1Dvn7pF4aIiftV70\/ch_3JTC14F4aIiftV700iFq2CHB\/rcpt_K7QsrFln9FgFnzUuBIiNdkkRYGxUL0X\",\"amount\":100}}",
7+
"attributes": {
8+
"ApproximateReceiveCount": "1",
9+
"SentTimestamp": "1523232000000",
10+
"SenderId": "123456789012",
11+
"ApproximateFirstReceiveTimestamp": "1523232000001"
12+
},
13+
"messageAttributes": {},
14+
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
15+
"eventSource": "aws:sqs",
16+
"eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
17+
"awsRegion": "us-east-1"
18+
}
19+
]
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from aws_lambda_powertools.utilities.jmespath_utils import envelopes, extract_data_from_envelope
2+
from aws_lambda_powertools.utilities.typing import LambdaContext
3+
4+
5+
def handler(event: dict, context: LambdaContext) -> dict:
6+
payload = extract_data_from_envelope(data=event, envelope=envelopes.SQS)
7+
customer_id = payload.get("customerId") # now deserialized
8+
9+
return {"customer_id": customer_id, "message": "success", "statusCode": 200}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}",
3+
"deeply_nested": [
4+
{
5+
"some_data": [
6+
1,
7+
2,
8+
3
9+
]
10+
}
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope
2+
from aws_lambda_powertools.utilities.typing import LambdaContext
3+
4+
5+
def handler(event: dict, context: LambdaContext) -> dict:
6+
payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)")
7+
customer_id = payload.get("customerId") # now deserialized
8+
9+
# also works for fetching and flattening deeply nested data
10+
some_data = extract_data_from_envelope(data=event, envelope="deeply_nested[*].some_data[]")
11+
12+
return {"customer_id": customer_id, "message": "success", "context": some_data, "statusCode": 200}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import base64
2+
import binascii
3+
import gzip
4+
import json
5+
6+
import powertools_base64_gzip_jmespath_schema as schemas
7+
from jmespath.exceptions import JMESPathTypeError
8+
9+
from aws_lambda_powertools.utilities.typing import LambdaContext
10+
from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate
11+
12+
13+
def lambda_handler(event, context: LambdaContext) -> dict:
14+
try:
15+
validate(event=event, schema=schemas.INPUT, envelope="powertools_base64_gzip(payload) | powertools_json(@)")
16+
17+
# Alternatively, extract_data_from_envelope works here too
18+
encoded_payload = base64.b64decode(event["payload"])
19+
uncompressed_payload = gzip.decompress(encoded_payload).decode()
20+
log: dict = json.loads(uncompressed_payload)
21+
22+
return {
23+
"message": "Logs processed",
24+
"log_group": log.get("logGroup"),
25+
"owner": log.get("owner"),
26+
"success": True,
27+
}
28+
29+
except JMESPathTypeError:
30+
return return_error_message("The powertools_base64_gzip() envelope function must match a valid path.")
31+
except binascii.Error:
32+
return return_error_message("Payload must be a valid base64 encoded string")
33+
except json.JSONDecodeError:
34+
return return_error_message("Payload must be valid JSON (base64 encoded).")
35+
except SchemaValidationError as exception:
36+
# SchemaValidationError indicates where a data mismatch is
37+
return return_error_message(str(exception))
38+
39+
40+
def return_error_message(message: str) -> dict:
41+
return {"message": message, "success": False}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"payload": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA=="
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
INPUT = {
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "http://example.com/example.json",
4+
"type": "object",
5+
"title": "Sample schema",
6+
"description": "The root schema comprises the entire JSON document.",
7+
"examples": [
8+
{
9+
"owner": "123456789012",
10+
"logGroup": "/aws/lambda/powertools-example",
11+
"logStream": "2022/08/07/[$LATEST]d3a8dcaffc7f4de2b8db132e3e106660",
12+
"logEvents": {},
13+
}
14+
],
15+
"required": ["owner", "logGroup", "logStream", "logEvents"],
16+
"properties": {
17+
"owner": {
18+
"$id": "#/properties/owner",
19+
"type": "string",
20+
"title": "The owner",
21+
"examples": ["123456789012"],
22+
"maxLength": 12,
23+
},
24+
"logGroup": {
25+
"$id": "#/properties/logGroup",
26+
"type": "string",
27+
"title": "The logGroup",
28+
"examples": ["/aws/lambda/powertools-example"],
29+
"maxLength": 100,
30+
},
31+
"logStream": {
32+
"$id": "#/properties/logStream",
33+
"type": "string",
34+
"title": "The logGroup",
35+
"examples": ["2022/08/07/[$LATEST]d3a8dcaffc7f4de2b8db132e3e106660"],
36+
"maxLength": 100,
37+
},
38+
"logEvents": {
39+
"$id": "#/properties/logEvents",
40+
"type": "array",
41+
"title": "The logEvents",
42+
"examples": [
43+
"{'id': 'eventId1', 'message': {'username': 'lessa', 'message': 'hello world'}, 'timestamp': 1440442987000}" # noqa E501
44+
],
45+
},
46+
},
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import base64
2+
import binascii
3+
import json
4+
from dataclasses import asdict, dataclass, field, is_dataclass
5+
from uuid import uuid4
6+
7+
import powertools_base64_jmespath_schema as schemas
8+
from jmespath.exceptions import JMESPathTypeError
9+
10+
from aws_lambda_powertools.utilities.typing import LambdaContext
11+
from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate
12+
13+
14+
@dataclass
15+
class Order:
16+
user_id: int
17+
product_id: int
18+
quantity: int
19+
price: float
20+
currency: str
21+
order_id: str = field(default_factory=lambda: f"{uuid4()}")
22+
23+
24+
class DataclassCustomEncoder(json.JSONEncoder):
25+
"""A custom JSON encoder to serialize dataclass obj"""
26+
27+
def default(self, obj):
28+
# Only called for values that aren't JSON serializable
29+
# where `obj` will be an instance of Todo in this example
30+
return asdict(obj) if is_dataclass(obj) else super().default(obj)
31+
32+
33+
def lambda_handler(event, context: LambdaContext) -> dict:
34+
35+
# Try to validate the schema
36+
try:
37+
validate(event=event, schema=schemas.INPUT, envelope="powertools_json(powertools_base64(payload))")
38+
39+
# alternatively, extract_data_from_envelope works here too
40+
payload_decoded = base64.b64decode(event["payload"]).decode()
41+
42+
order_payload: dict = json.loads(payload_decoded)
43+
44+
return {
45+
"order": json.dumps(Order(**order_payload), cls=DataclassCustomEncoder),
46+
"message": "order created",
47+
"success": True,
48+
}
49+
except JMESPathTypeError:
50+
return return_error_message(
51+
"The powertools_json(powertools_base64()) envelope function must match a valid path."
52+
)
53+
except binascii.Error:
54+
return return_error_message("Payload must be a valid base64 encoded string")
55+
except json.JSONDecodeError:
56+
return return_error_message("Payload must be valid JSON (base64 encoded).")
57+
except SchemaValidationError as exception:
58+
# SchemaValidationError indicates where a data mismatch is
59+
return return_error_message(str(exception))
60+
61+
62+
def return_error_message(message: str) -> dict:
63+
return {"order": None, "message": message, "success": False}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"payload":"eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0="
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
INPUT = {
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "http://example.com/example.json",
4+
"type": "object",
5+
"title": "Sample order schema",
6+
"description": "The root schema comprises the entire JSON document.",
7+
"examples": [{"user_id": 123, "product_id": 1, "quantity": 2, "price": 10.40, "currency": "USD"}],
8+
"required": ["user_id", "product_id", "quantity", "price", "currency"],
9+
"properties": {
10+
"user_id": {
11+
"$id": "#/properties/user_id",
12+
"type": "integer",
13+
"title": "The unique identifier of the user",
14+
"examples": [123],
15+
"maxLength": 10,
16+
},
17+
"product_id": {
18+
"$id": "#/properties/product_id",
19+
"type": "integer",
20+
"title": "The unique identifier of the product",
21+
"examples": [1],
22+
"maxLength": 10,
23+
},
24+
"quantity": {
25+
"$id": "#/properties/quantity",
26+
"type": "integer",
27+
"title": "The quantity of the product",
28+
"examples": [2],
29+
"maxLength": 10,
30+
},
31+
"price": {
32+
"$id": "#/properties/price",
33+
"type": "number",
34+
"title": "The individual price of the product",
35+
"examples": [10.40],
36+
"maxLength": 10,
37+
},
38+
"currency": {
39+
"$id": "#/properties/currency",
40+
"type": "string",
41+
"title": "The currency",
42+
"examples": ["The currency of the order"],
43+
"maxLength": 100,
44+
},
45+
},
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"Records": [
3+
{
4+
"user": "integration-kafka",
5+
"datetime": "2022-01-01T00:00:00.000Z",
6+
"log": "/QGIMjAyMi8wNi8xNiAxNjoyNTowMCBbY3JpdF0gMzA1MTg5MCMNCPBEOiAqMSBjb25uZWN0KCkg\ndG8gMTI3LjAuMC4xOjUwMDAgZmFpbGVkICgxMzogUGVybWlzc2lvbiBkZW5pZWQpIHdoaWxlEUEI\naW5nAUJAdXBzdHJlYW0sIGNsaWVudDoZVKgsIHNlcnZlcjogXywgcmVxdWVzdDogIk9QVElPTlMg\nLyBIVFRQLzEuMSIsFUckOiAiaHR0cDovLzabABQvIiwgaG8FQDAxMjcuMC4wLjE6ODEi\n"
7+
},
8+
{
9+
"user": "integration-kafka",
10+
"datetime": "2022-01-01T00:00:01.000Z",
11+
"log": "tQHwnDEyNy4wLjAuMSAtIC0gWzE2L0p1bi8yMDIyOjE2OjMwOjE5ICswMTAwXSAiT1BUSU9OUyAv\nIEhUVFAvMS4xIiAyMDQgMCAiLSIgIk1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NCkgQXBw\nbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwMi4BmUwwIFNhZmFy\naS81MzcuMzYiICItIg==\n"
12+
}
13+
]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import base64
2+
import binascii
3+
4+
import snappy
5+
from jmespath.exceptions import JMESPathTypeError
6+
from jmespath.functions import signature
7+
8+
from aws_lambda_powertools.utilities.jmespath_utils import PowertoolsFunctions, extract_data_from_envelope
9+
10+
11+
class CustomFunctions(PowertoolsFunctions):
12+
# only decode if value is a string
13+
# see supported data types: https://jmespath.org/specification.html#built-in-functions
14+
@signature({"types": ["string"]})
15+
def _func_decode_snappy_compression(self, payload: str):
16+
decoded: bytes = base64.b64decode(payload)
17+
return snappy.uncompress(decoded)
18+
19+
20+
custom_jmespath_options = {"custom_functions": CustomFunctions()}
21+
22+
23+
def lambda_handler(event, context) -> dict:
24+
25+
try:
26+
logs = []
27+
logs.append(
28+
extract_data_from_envelope(
29+
data=event,
30+
# NOTE: Use the prefix `_func_` before the name of the function
31+
envelope="Records[*].decode_snappy_compression(log)",
32+
jmespath_options=custom_jmespath_options,
33+
)
34+
)
35+
return {"logs": logs, "message": "Extracted messages", "success": True}
36+
except JMESPathTypeError:
37+
return return_error_message("The envelope function must match a valid path.")
38+
except snappy.UncompressError:
39+
return return_error_message("Log must be a valid snappy compressed binary")
40+
except binascii.Error:
41+
return return_error_message("Log must be a valid base64 encoded string")
42+
43+
44+
def return_error_message(message: str) -> dict:
45+
return {"logs": None, "message": message, "success": False}

0 commit comments

Comments
 (0)