Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose jmespath powertools functions #736

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aws_lambda_powertools/logging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def set_package_logger(
-------
**Enables debug logging for AWS Lambda Powertools package**

>>> from aws_lambda_powertools.logging.logger import set_package_logger
>>> aws_lambda_powertools.logging.logger import set_package_logger
>>> set_package_logger()

Parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from botocore.config import Config

from aws_lambda_powertools.utilities import jmespath_utils
from aws_lambda_powertools.utilities.parameters import AppConfigProvider, GetParameterError, TransformParameterError

from ... import Logger
from ...shared import jmespath_utils
from .base import StoreProvider
from .exceptions import ConfigurationStoreError, StoreClientError

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.cache_dict import LRUDict
from aws_lambda_powertools.shared.jmespath_utils import PowertoolsFunctions
from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
from aws_lambda_powertools.utilities.idempotency.exceptions import (
Expand All @@ -25,6 +24,7 @@
IdempotencyKeyError,
IdempotencyValidationError,
)
from aws_lambda_powertools.utilities.jmespath_utils import PowertoolsFunctions

logger = logging.getLogger(__name__)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,27 @@ def _func_powertools_base64_gzip(self, value):
return uncompressed.decode()


def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict]) -> Any:
"""Searches data using JMESPath expression
def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict] = None) -> Any:
"""Searches and extracts data using JMESPath

Envelope being the JMESPath expression to extract the data you're after

Built-in JMESPath functions include: powertools_json, powertools_base64, powertools_base64_gzip

Examples
--------

**Deserialize JSON string and extracts data from body key**

from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope
from aws_lambda_powertools.utilities.typing import LambdaContext


def handler(event: dict, context: LambdaContext):
# event = {"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}"} # noqa: E800
payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)")
customer = payload.get("customerId") # now deserialized
...

Parameters
----------
Expand All @@ -42,6 +61,7 @@ def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_o
jmespath_options : Dict
Alternative JMESPath options to be included when filtering expr


Returns
-------
Any
Expand Down
8 changes: 8 additions & 0 deletions aws_lambda_powertools/utilities/jmespath_utils/envelopes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
API_GATEWAY_REST = "powertools_json(body)"
API_GATEWAY_HTTP = API_GATEWAY_REST
SQS = "Records[*].powertools_json(body)"
SNS = "Records[0].Sns.Message | powertools_json(@)"
EVENTBRIDGE = "detail"
CLOUDWATCH_EVENTS_SCHEDULED = EVENTBRIDGE
KINESIS_DATA_STREAM = "Records[*].kinesis.powertools_json(powertools_base64(data))"
CLOUDWATCH_LOGS = "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]"
3 changes: 2 additions & 1 deletion aws_lambda_powertools/utilities/validation/validator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
from typing import Any, Callable, Dict, Optional, Union

from aws_lambda_powertools.utilities import jmespath_utils

from ...middleware_factory import lambda_handler_decorator
from ...shared import jmespath_utils
from .base import validate_data_against_schema

logger = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Imagine the function executes successfully, but the client never receives the re
!!! warning "Idempotency for JSON payloads"
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.

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.
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.

=== "payment.py"

Expand Down
131 changes: 115 additions & 16 deletions docs/utilities/jmespath_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,106 @@ title: JMESPath Functions
description: Utility
---

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.
!!! tip "JMESPath is a query language for JSON used by AWS CLI, AWS Python SDK, and AWS Lambda Powertools for Python."

## Built-in JMESPath functions
Built-in [JMESPath](https://jmespath.org/){target="_blank"} Functions to easily deserialize common encoded JSON payloads in Lambda functions.

## Key features

* Deserialize JSON from JSON strings, base64, and compressed data
* Use JMESPath to extract and combine data recursively

## Getting started

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.

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.

### Extracting data

You can use the `extract_data_from_envelope` function along with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}.

=== "app.py"

```python hl_lines="1 7"
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope

from aws_lambda_powertools.utilities.typing import LambdaContext


def handler(event: dict, context: LambdaContext):
payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)")
customer = payload.get("customerId") # now deserialized
...
```

=== "event.json"

```json
{
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}"
}
```

### Built-in envelopes

We provide built-in envelopes for popular JMESPath expressions used when looking to decode/deserialize JSON objects within AWS Lambda Event Sources.

=== "app.py"

```python hl_lines="1 7"
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope, envelopes

from aws_lambda_powertools.utilities.typing import LambdaContext


def handler(event: dict, context: LambdaContext):
payload = extract_data_from_envelope(data=event, envelope=envelopes.SNS)
customer = payload.get("customerId") # now deserialized
...
```

=== "event.json"

```json hl_lines="6"
{
"Records": [
{
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
"receiptHandle": "MessageReceiptHandle",
"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}}",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1523232000000",
"SenderId": "123456789012",
"ApproximateFirstReceiveTimestamp": "1523232000001"
},
"messageAttributes": {},
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
"awsRegion": "us-east-1"
}
]
}
```

These are all built-in envelopes you can use along with their expression as a reference:

Envelope | JMESPath expression
------------------------------------------------- | ---------------------------------------------------------------------------------
**`API_GATEWAY_REST`** | `powertools_json(body)`
**`API_GATEWAY_HTTP`** | `API_GATEWAY_REST`
**`SQS`** | `Records[*].powertools_json(body)`
**`SNS`** | `Records[0].Sns.Message | powertools_json(@)`
**`EVENTBRIDGE`** | `detail`
**`CLOUDWATCH_EVENTS_SCHEDULED`** | `EVENTBRIDGE`
**`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))`
**`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]`

## Advanced

### Built-in JMESPath functions
You can use our built-in JMESPath functions within your expressions to do exactly that to decode JSON Strings, base64, and uncompress gzip data.

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

This will **replace all provided built-in functions such as `powertools_json`, so you will no longer be able to use them**.

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.

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

```python hl_lines="2 6-10 14"
from aws_lambda_powertools.utilities.validation import validator
from jmespath import functions
=== "custom_jmespath_function.py"

import schemas
```python hl_lines="2-3 6-9 11 17"
from aws_lambda_powertools.utilities.jmespath_utils import (
PowertoolsFunctions, extract_data_from_envelope)
from jmespath.functions import signature

class CustomFunctions(functions.Functions):

@functions.signature({'types': ['string']})
class CustomFunctions(PowertoolsFunctions):
@signature({'types': ['string']}) # Only decode if value is a string
def _func_special_decoder(self, s):
return my_custom_decoder_logic(s)

custom_jmespath_options = {"custom_functions": CustomFunctions()}

@validator(schema=schemas.INPUT, jmespath_options=**custom_jmespath_options)
def handler(event, context):
return event
# use the custom name after `_func_`
extract_data_from_envelope(data=event,
envelope="special_decoder(body)",
jmespath_options=**custom_jmespath_options)
...
```

=== "schemas.py"
=== "event.json"

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