Skip to content

Commit af36fb5

Browse files
authored
feat: expose jmespath powertools functions (#736)
1 parent dec3c88 commit af36fb5

File tree

10 files changed

+229
-41
lines changed

10 files changed

+229
-41
lines changed

aws_lambda_powertools/logging/logger.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ def set_package_logger(
446446
-------
447447
**Enables debug logging for AWS Lambda Powertools package**
448448
449-
>>> from aws_lambda_powertools.logging.logger import set_package_logger
449+
>>> aws_lambda_powertools.logging.logger import set_package_logger
450450
>>> set_package_logger()
451451
452452
Parameters

aws_lambda_powertools/utilities/feature_flags/appconfig.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from botocore.config import Config
66

7+
from aws_lambda_powertools.utilities import jmespath_utils
78
from aws_lambda_powertools.utilities.parameters import AppConfigProvider, GetParameterError, TransformParameterError
89

910
from ... import Logger
10-
from ...shared import jmespath_utils
1111
from .base import StoreProvider
1212
from .exceptions import ConfigurationStoreError, StoreClientError
1313

aws_lambda_powertools/utilities/idempotency/persistence/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from aws_lambda_powertools.shared import constants
1818
from aws_lambda_powertools.shared.cache_dict import LRUDict
19-
from aws_lambda_powertools.shared.jmespath_utils import PowertoolsFunctions
2019
from aws_lambda_powertools.shared.json_encoder import Encoder
2120
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
2221
from aws_lambda_powertools.utilities.idempotency.exceptions import (
@@ -25,6 +24,7 @@
2524
IdempotencyKeyError,
2625
IdempotencyValidationError,
2726
)
27+
from aws_lambda_powertools.utilities.jmespath_utils import PowertoolsFunctions
2828

2929
logger = logging.getLogger(__name__)
3030

aws_lambda_powertools/shared/jmespath_utils.py renamed to aws_lambda_powertools/utilities/jmespath_utils/__init__.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,27 @@ def _func_powertools_base64_gzip(self, value):
3030
return uncompressed.decode()
3131

3232

33-
def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict]) -> Any:
34-
"""Searches data using JMESPath expression
33+
def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict] = None) -> Any:
34+
"""Searches and extracts data using JMESPath
35+
36+
Envelope being the JMESPath expression to extract the data you're after
37+
38+
Built-in JMESPath functions include: powertools_json, powertools_base64, powertools_base64_gzip
39+
40+
Examples
41+
--------
42+
43+
**Deserialize JSON string and extracts data from body key**
44+
45+
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope
46+
from aws_lambda_powertools.utilities.typing import LambdaContext
47+
48+
49+
def handler(event: dict, context: LambdaContext):
50+
# event = {"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}"} # noqa: E800
51+
payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)")
52+
customer = payload.get("customerId") # now deserialized
53+
...
3554
3655
Parameters
3756
----------
@@ -42,6 +61,7 @@ def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_o
4261
jmespath_options : Dict
4362
Alternative JMESPath options to be included when filtering expr
4463
64+
4565
Returns
4666
-------
4767
Any
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
API_GATEWAY_REST = "powertools_json(body)"
2+
API_GATEWAY_HTTP = API_GATEWAY_REST
3+
SQS = "Records[*].powertools_json(body)"
4+
SNS = "Records[0].Sns.Message | powertools_json(@)"
5+
EVENTBRIDGE = "detail"
6+
CLOUDWATCH_EVENTS_SCHEDULED = EVENTBRIDGE
7+
KINESIS_DATA_STREAM = "Records[*].kinesis.powertools_json(powertools_base64(data))"
8+
CLOUDWATCH_LOGS = "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]"

aws_lambda_powertools/utilities/validation/validator.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import logging
22
from typing import Any, Callable, Dict, Optional, Union
33

4+
from aws_lambda_powertools.utilities import jmespath_utils
5+
46
from ...middleware_factory import lambda_handler_decorator
5-
from ...shared import jmespath_utils
67
from .base import validate_data_against_schema
78

89
logger = logging.getLogger(__name__)

docs/utilities/idempotency.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Imagine the function executes successfully, but the client never receives the re
209209
!!! warning "Idempotency for JSON payloads"
210210
The payload extracted by the `event_key_jmespath` is treated as a string by default, so will be sensitive to differences in whitespace even when the JSON payload itself is identical.
211211

212-
To alter this behaviour, we can use the [JMESPath built-in function](/utilities/jmespath_functions) *powertools_json()* to treat the payload as a JSON object rather than a string.
212+
To alter this behaviour, we can use the [JMESPath built-in function](jmespath_functions.md#powertools_json-function) `powertools_json()` to treat the payload as a JSON object rather than a string.
213213

214214
=== "payment.py"
215215

docs/utilities/jmespath_functions.md

+115-16
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,106 @@ title: JMESPath Functions
33
description: Utility
44
---
55

6-
You might have events or responses that contain non-encoded JSON, where you need to decode so that you can access portions of the object or ensure the Powertools utility receives a JSON object. This is a common use case when using the [validation](/utilities/validation) or [idempotency](/utilities/idempotency) utilities.
6+
!!! tip "JMESPath is a query language for JSON used by AWS CLI, AWS Python SDK, and AWS Lambda Powertools for Python."
77

8-
## Built-in JMESPath functions
8+
Built-in [JMESPath](https://jmespath.org/){target="_blank"} Functions to easily deserialize common encoded JSON payloads in Lambda functions.
9+
10+
## Key features
11+
12+
* Deserialize JSON from JSON strings, base64, and compressed data
13+
* Use JMESPath to extract and combine data recursively
14+
15+
## Getting started
16+
17+
You might have events that contains encoded JSON payloads as string, base64, or even in compressed format. It is a common use case to decode and extract them partially or fully as part of your Lambda function invocation.
18+
19+
Lambda Powertools also have utilities like [validation](validation.md), [idempotency](idempotency.md), or [feature flags](feature_flags.md) where you might need to extract a portion of your data before using them.
20+
21+
### Extracting data
22+
23+
You can use the `extract_data_from_envelope` function along with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}.
24+
25+
=== "app.py"
26+
27+
```python hl_lines="1 7"
28+
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope
29+
30+
from aws_lambda_powertools.utilities.typing import LambdaContext
31+
32+
33+
def handler(event: dict, context: LambdaContext):
34+
payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)")
35+
customer = payload.get("customerId") # now deserialized
36+
...
37+
```
38+
39+
=== "event.json"
40+
41+
```json
42+
{
43+
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}"
44+
}
45+
```
46+
47+
### Built-in envelopes
48+
49+
We provide built-in envelopes for popular JMESPath expressions used when looking to decode/deserialize JSON objects within AWS Lambda Event Sources.
50+
51+
=== "app.py"
52+
53+
```python hl_lines="1 7"
54+
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope, envelopes
55+
56+
from aws_lambda_powertools.utilities.typing import LambdaContext
57+
58+
59+
def handler(event: dict, context: LambdaContext):
60+
payload = extract_data_from_envelope(data=event, envelope=envelopes.SNS)
61+
customer = payload.get("customerId") # now deserialized
62+
...
63+
```
64+
65+
=== "event.json"
66+
67+
```json hl_lines="6"
68+
{
69+
"Records": [
70+
{
71+
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
72+
"receiptHandle": "MessageReceiptHandle",
73+
"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}}",
74+
"attributes": {
75+
"ApproximateReceiveCount": "1",
76+
"SentTimestamp": "1523232000000",
77+
"SenderId": "123456789012",
78+
"ApproximateFirstReceiveTimestamp": "1523232000001"
79+
},
80+
"messageAttributes": {},
81+
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
82+
"eventSource": "aws:sqs",
83+
"eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
84+
"awsRegion": "us-east-1"
85+
}
86+
]
87+
}
88+
```
89+
90+
These are all built-in envelopes you can use along with their expression as a reference:
91+
92+
Envelope | JMESPath expression
93+
------------------------------------------------- | ---------------------------------------------------------------------------------
94+
**`API_GATEWAY_REST`** | `powertools_json(body)`
95+
**`API_GATEWAY_HTTP`** | `API_GATEWAY_REST`
96+
**`SQS`** | `Records[*].powertools_json(body)`
97+
**`SNS`** | `Records[0].Sns.Message | powertools_json(@)`
98+
**`EVENTBRIDGE`** | `detail`
99+
**`CLOUDWATCH_EVENTS_SCHEDULED`** | `EVENTBRIDGE`
100+
**`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))`
101+
**`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]`
102+
103+
## Advanced
104+
105+
### Built-in JMESPath functions
9106
You can use our built-in JMESPath functions within your expressions to do exactly that to decode JSON Strings, base64, and uncompress gzip data.
10107

11108
!!! info
@@ -134,33 +231,35 @@ This sample will decompress and decode base64 data, then use JMESPath pipeline e
134231
!!! warning
135232
This should only be used for advanced use cases where you have special formats not covered by the built-in functions.
136233

137-
This will **replace all provided built-in functions such as `powertools_json`, so you will no longer be able to use them**.
138-
139234
For special binary formats that you want to decode before applying JSON Schema validation, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"} and any additional option via `jmespath_options` param.
140235

141-
=== "custom_jmespath_function.py"
236+
In order to keep the built-in functions from Powertools, you can subclass from `PowertoolsFunctions`:
142237

143-
```python hl_lines="2 6-10 14"
144-
from aws_lambda_powertools.utilities.validation import validator
145-
from jmespath import functions
238+
=== "custom_jmespath_function.py"
146239

147-
import schemas
240+
```python hl_lines="2-3 6-9 11 17"
241+
from aws_lambda_powertools.utilities.jmespath_utils import (
242+
PowertoolsFunctions, extract_data_from_envelope)
243+
from jmespath.functions import signature
148244

149-
class CustomFunctions(functions.Functions):
150245

151-
@functions.signature({'types': ['string']})
246+
class CustomFunctions(PowertoolsFunctions):
247+
@signature({'types': ['string']}) # Only decode if value is a string
152248
def _func_special_decoder(self, s):
153249
return my_custom_decoder_logic(s)
154250

155251
custom_jmespath_options = {"custom_functions": CustomFunctions()}
156252

157-
@validator(schema=schemas.INPUT, jmespath_options=**custom_jmespath_options)
158253
def handler(event, context):
159-
return event
254+
# use the custom name after `_func_`
255+
extract_data_from_envelope(data=event,
256+
envelope="special_decoder(body)",
257+
jmespath_options=**custom_jmespath_options)
258+
...
160259
```
161260

162-
=== "schemas.py"
261+
=== "event.json"
163262

164-
```python hl_lines="7 14 16 23 39 45 47 52"
165-
--8<-- "docs/shared/validation_basic_jsonschema.py"
263+
```json
264+
{"body": "custom_encoded_data"}
166265
```

0 commit comments

Comments
 (0)