Skip to content
Merged
Prev Previous commit
Next Next commit
feat(response_hook): review items
  • Loading branch information
walmsles committed Apr 3, 2024
commit 8ea0c4fc034f670603da66413aaf74e2480df448
5 changes: 3 additions & 2 deletions aws_lambda_powertools/utilities/idempotency/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Utility for adding idempotency to lambda functions
"""

from aws_lambda_powertools.utilities.idempotency.config import IdempotentHookData, IdempotentHookFunction
from aws_lambda_powertools.utilities.idempotency.hook import (
IdempotentHookFunction,
)
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
BasePersistenceLayer,
)
Expand All @@ -18,6 +20,5 @@
"idempotent",
"idempotent_function",
"IdempotencyConfig",
"IdempotentHookData",
"IdempotentHookFunction",
)
9 changes: 5 additions & 4 deletions aws_lambda_powertools/utilities/idempotency/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from aws_lambda_powertools.utilities.idempotency.config import (
IdempotencyConfig,
IdempotentHookData,
)
from aws_lambda_powertools.utilities.idempotency.exceptions import (
IdempotencyAlreadyInProgressError,
Expand Down Expand Up @@ -230,12 +229,14 @@ def _handle_for_status(self, data_record: DataRecord) -> Optional[Any]:
)
response_dict: Optional[dict] = data_record.response_json_as_dict()
if response_dict is not None:
serialized_response = self.output_serializer.from_dict(response_dict)
if self.config.response_hook is not None:
return self.config.response_hook(
self.output_serializer.from_dict(response_dict),
IdempotentHookData(data_record),
serialized_response,
data_record,
)
return self.output_serializer.from_dict(response_dict)
return serialized_response

return None

def _get_function_response(self):
Expand Down
40 changes: 2 additions & 38 deletions aws_lambda_powertools/utilities/idempotency/config.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,9 @@
from typing import Any, Dict, Optional
from typing import Dict, Optional

from aws_lambda_powertools.shared.types import Protocol
from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord
from aws_lambda_powertools.utilities.idempotency import IdempotentHookFunction
from aws_lambda_powertools.utilities.typing import LambdaContext


class IdempotentHookData:
"""
Idempotent Hook Data

Contains data relevant to the current Idempotent record which matches the current request.
All IdempotentHook functions will be passed this data as well as the current Response.
"""

def __init__(self, data_record: DataRecord) -> None:
self._idempotency_key = data_record.idempotency_key
self._status = data_record.status
self._expiry_timestamp = data_record.expiry_timestamp

@property
def idempotency_key(self) -> str:
return self._idempotency_key

@property
def status(self) -> str:
return self._status

@property
def expiry_timestamp(self) -> Optional[int]:
return self._expiry_timestamp


class IdempotentHookFunction(Protocol):
"""
The IdempotentHookFunction.
This class defines the calling signature for IdempotentHookFunction callbacks.
"""

def __call__(self, response: Any, idempotent_data: IdempotentHookData) -> Any: ...


class IdempotencyConfig:
def __init__(
self,
Expand Down
13 changes: 13 additions & 0 deletions aws_lambda_powertools/utilities/idempotency/hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Any

from aws_lambda_powertools.shared.types import Protocol
from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord


class IdempotentHookFunction(Protocol):
"""
The IdempotentHookFunction.
This class defines the calling signature for IdempotentHookFunction callbacks.
"""

def __call__(self, response: Any, idempotent_data: DataRecord) -> Any: ...
28 changes: 14 additions & 14 deletions docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,16 +699,16 @@ For advanced configurations, such as setting up SSL certificates or customizing

Idempotent decorator can be further configured with **`IdempotencyConfig`** as seen in the previous example. These are the available options for further configuration

| Parameter | Default | Description |
|---------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **event_key_jmespath** | `""` | JMESPath expression to extract the idempotency key from the event record using [built-in functions](./jmespath_functions.md#built-in-jmespath-functions){target="_blank"} |
| **payload_validation_jmespath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event while the event payload |
| **raise_on_no_idempotency_key** | `False` | Raise exception if no idempotency key was found in the request |
| **expires_after_seconds** | 3600 | The number of seconds to wait before a record is expired |
| **use_local_cache** | `False` | Whether to locally cache idempotency results |
| **local_cache_max_items** | 256 | Max number of items to store in local cache |
| **hash_function** | `md5` | Function to use for calculating hashes, as provided by [hashlib](https://docs.python.org/3/library/hashlib.html){target="_blank" rel="nofollow"} in the standard library. |
| **response_hook** | `None` | Function to use for processing the stored Idempotent response. This function hook is called when an already returned response is found. See [Modifying The Idempotent Response](idempotency.md#modifying-the-idempotent-repsonse) |
| Parameter | Default | Description |
|---------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **event_key_jmespath** | `""` | JMESPath expression to extract the idempotency key from the event record using [built-in functions](./jmespath_functions.md#built-in-jmespath-functions){target="_blank"} |
| **payload_validation_jmespath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event while the event payload |
| **raise_on_no_idempotency_key** | `False` | Raise exception if no idempotency key was found in the request |
| **expires_after_seconds** | 3600 | The number of seconds to wait before a record is expired |
| **use_local_cache** | `False` | Whether to locally cache idempotency results |
| **local_cache_max_items** | 256 | Max number of items to store in local cache |
| **hash_function** | `md5` | Function to use for calculating hashes, as provided by [hashlib](https://docs.python.org/3/library/hashlib.html){target="_blank" rel="nofollow"} in the standard library. |
| **response_hook** | `None` | Function to use for processing the stored Idempotent response. This function hook is called when an existing idempotent response is found. See [Manipulating The Idempotent Response](idempotency.md#manipulating-the-idempotent-response) |

### Handling concurrent executions with the same payload

Expand Down Expand Up @@ -910,15 +910,15 @@ You can create your own persistent store from scratch by inheriting the `BasePer

For example, the `_put_record` method needs to raise an exception if a non-expired record already exists in the data store with a matching key.

### Modifying the Idempotent Repsonse
### Manipulating the Idempotent Response

The IdempotentConfig allows you to specify a _**response_hook**_ which is a function that will be called when an idempotent response is loaded from the PersistenceStore.
The IdempotentConfig allows you to specify a _**response_hook**_ which is a function that will be called when an already returned response is loaded from the PersistenceStore. The Hook function will be called with the current de-serialized response object and the Idempotent DataRecord.

You can provide the response_hook using _**IdempotentConfig**_.
You can provide the response_hook using_**IdempotentConfig**_.

=== "Using an Idempotent Response Hook"

```python hl_lines="10-15 19"
```python hl_lines="15-23 28"
--8<-- "examples/idempotency/src/working_with_response_hook.py"
```

Expand Down
14 changes: 10 additions & 4 deletions examples/idempotency/src/working_with_response_hook.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from datetime import datetime
from typing import Dict

from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer,
IdempotencyConfig,
IdempotentHookData,
idempotent,
)
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
DataRecord,
)
from aws_lambda_powertools.utilities.typing import LambdaContext


def my_response_hook(response: Dict, idempotent_data: IdempotentHookData) -> Dict:
# How to add a field to the response
response["is_idempotent_response"] = True
def my_response_hook(response: Dict, idempotent_data: DataRecord) -> Dict:
# Return inserted Header data into the Idempotent Response
expiry_time = datetime.fromtimestamp(idempotent_data.expiry_timestamp)

response["headers"]["x-idempotent-key"] = idempotent_data.idempotency_key
response["headers"]["x-idempotent-expiration"] = expiry_time.isoformat()

# Must return the response here
return response
Expand Down
3 changes: 1 addition & 2 deletions tests/functional/idempotency/test_idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
IdempotencyHandler,
_prepare_data,
)
from aws_lambda_powertools.utilities.idempotency.config import IdempotentHookData
from aws_lambda_powertools.utilities.idempotency.exceptions import (
IdempotencyAlreadyInProgressError,
IdempotencyInconsistentStateError,
Expand Down Expand Up @@ -2043,7 +2042,7 @@ def test_idempotent_lambda_already_completed_response_hook_is_called(
Test idempotent decorator where event with matching event key has already been successfully processed
"""

def idempotent_response_hook(response: Any, idempotent_data: IdempotentHookData) -> Any:
def idempotent_response_hook(response: Any, idempotent_data: DataRecord) -> Any:
"""Modify the response provided by adding a new key"""
response["idempotent_response"] = True

Expand Down