Skip to content

Commit 7be425e

Browse files
docs(jmespath_util): snippets split, improved, and lint (#1419)
Co-authored-by: heitorlessa <lessa@amazon.co.uk>
1 parent a71a0e5 commit 7be425e

18 files changed

+575
-163
lines changed

docs/utilities/jmespath_functions.md

Lines changed: 91 additions & 163 deletions
Large diffs are not rendered by default.
Lines changed: 20 additions & 0 deletions
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+
}
Lines changed: 9 additions & 0 deletions
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}
Lines changed: 12 additions & 0 deletions
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+
}
Lines changed: 12 additions & 0 deletions
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}
Lines changed: 41 additions & 0 deletions
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}
Lines changed: 3 additions & 0 deletions
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+
}
Lines changed: 47 additions & 0 deletions
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+
}
Lines changed: 63 additions & 0 deletions
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}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"payload":"eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0="
3+
}
Lines changed: 46 additions & 0 deletions
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+
}
Lines changed: 14 additions & 0 deletions
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+
}
Lines changed: 45 additions & 0 deletions
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}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"version":"2.0",
3+
"routeKey":"ANY /createpayment",
4+
"rawPath":"/createpayment",
5+
"rawQueryString":"",
6+
"headers": {
7+
"Header1": "value1",
8+
"Header2": "value2"
9+
},
10+
"requestContext":{
11+
"accountId":"123456789012",
12+
"apiId":"api-id",
13+
"domainName":"id.execute-api.us-east-1.amazonaws.com",
14+
"domainPrefix":"id",
15+
"http":{
16+
"method":"POST",
17+
"path":"/createpayment",
18+
"protocol":"HTTP/1.1",
19+
"sourceIp":"ip",
20+
"userAgent":"agent"
21+
},
22+
"requestId":"id",
23+
"routeKey":"ANY /createpayment",
24+
"stage":"$default",
25+
"time":"10/Feb/2021:13:40:43 +0000",
26+
"timeEpoch":1612964443723
27+
},
28+
"body":"{\"user\":\"xyz\",\"product_id\":\"123456789\"}",
29+
"isBase64Encoded":false
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
from uuid import uuid4
3+
4+
import requests
5+
6+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
7+
8+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
9+
10+
# Treat everything under the "body" key
11+
# in the event json object as our payload
12+
config = IdempotencyConfig(event_key_jmespath="powertools_json(body)")
13+
14+
15+
class PaymentError(Exception):
16+
...
17+
18+
19+
@idempotent(config=config, persistence_store=persistence_layer)
20+
def handler(event, context) -> dict:
21+
body = json.loads(event["body"])
22+
try:
23+
payment = create_subscription_payment(user=body["user"], product_id=body["product_id"])
24+
return {"payment_id": payment.id, "message": "success", "statusCode": 200}
25+
except requests.HTTPError as e:
26+
raise PaymentError("Unable to create payment subscription") from e
27+
28+
29+
def create_subscription_payment(user: str, product_id: str) -> dict:
30+
payload = {"user": user, "product_id": product_id}
31+
ret: requests.Response = requests.post(url="https://httpbin.org/anything", data=payload)
32+
ret.raise_for_status()
33+
34+
return {"id": f"{uuid4()}", "message": "paid"}

0 commit comments

Comments
 (0)