-
Notifications
You must be signed in to change notification settings - Fork 421
refactor(idempotent): Change UX to use a config class for non-persistence related features #306
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
Changes from all commits
dfb5b0f
b482152
2174b91
2512cda
131bde4
e74a489
07e8a06
e2c96eb
7d4304b
0fc4ee2
eb4787c
b041f3f
8c2350e
1254b06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from typing import Dict | ||
|
||
|
||
class IdempotencyConfig: | ||
def __init__( | ||
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks great - Hopefully when 3.6 is EOL we'll be able to move to dataclasses to make this easier too, including having a generic Config that auto-discovers env vars based on config option name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @heitorlessa should we add a mini project for ideas? |
||
self, | ||
event_key_jmespath: str = "", | ||
payload_validation_jmespath: str = "", | ||
jmespath_options: Dict = None, | ||
raise_on_no_idempotency_key: bool = False, | ||
expires_after_seconds: int = 60 * 60, # 1 hour default | ||
use_local_cache: bool = False, | ||
local_cache_max_items: int = 256, | ||
hash_function: str = "md5", | ||
): | ||
""" | ||
Initialize the base persistence layer | ||
|
||
Parameters | ||
---------- | ||
event_key_jmespath: str | ||
A jmespath expression to extract the idempotency key from the event record | ||
payload_validation_jmespath: str | ||
A jmespath expression to extract the payload to be validated from the event record | ||
raise_on_no_idempotency_key: bool, optional | ||
Raise exception if no idempotency key was found in the request, by default False | ||
expires_after_seconds: int | ||
The number of seconds to wait before a record is expired | ||
use_local_cache: bool, optional | ||
Whether to locally cache idempotency results, by default False | ||
local_cache_max_items: int, optional | ||
Max number of items to store in local cache, by default 1024 | ||
hash_function: str, optional | ||
Function to use for calculating hashes, by default md5. | ||
""" | ||
self.event_key_jmespath = event_key_jmespath | ||
self.payload_validation_jmespath = payload_validation_jmespath | ||
self.jmespath_options = jmespath_options | ||
self.raise_on_no_idempotency_key = raise_on_no_idempotency_key | ||
self.expires_after_seconds = expires_after_seconds | ||
self.use_local_cache = use_local_cache | ||
self.local_cache_max_items = local_cache_max_items | ||
self.hash_function = hash_function |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from typing import Any, Callable, Dict, Optional | ||
|
||
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator | ||
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig | ||
from aws_lambda_powertools.utilities.idempotency.exceptions import ( | ||
IdempotencyAlreadyInProgressError, | ||
IdempotencyInconsistentStateError, | ||
|
@@ -29,6 +30,7 @@ def idempotent( | |
event: Dict[str, Any], | ||
context: LambdaContext, | ||
persistence_store: BasePersistenceLayer, | ||
config: IdempotencyConfig = None, | ||
) -> Any: | ||
""" | ||
Middleware to handle idempotency | ||
|
@@ -43,20 +45,25 @@ def idempotent( | |
Lambda's Context | ||
persistence_store: BasePersistenceLayer | ||
Instance of BasePersistenceLayer to store data | ||
config: IdempotencyConfig | ||
Configutation | ||
|
||
Examples | ||
-------- | ||
**Processes Lambda's event in an idempotent manner** | ||
>>> from aws_lambda_powertools.utilities.idempotency import idempotent, DynamoDBPersistenceLayer | ||
>>> from aws_lambda_powertools.utilities.idempotency import ( | ||
>>> idempotent, DynamoDBPersistenceLayer, IdempotencyConfig | ||
>>> ) | ||
>>> | ||
>>> persistence_store = DynamoDBPersistenceLayer(event_key_jmespath="body", table_name="idempotency_store") | ||
>>> idem_config=IdempotencyConfig(event_key_jmespath="body") | ||
>>> persistence_layer = DynamoDBPersistenceLayer(table_name="idempotency_store") | ||
>>> | ||
>>> @idempotent(persistence_store=persistence_store) | ||
>>> @idempotent(config=idem_config, persistence_store=persistence_layer) | ||
>>> def handler(event, context): | ||
>>> return {"StatusCode": 200} | ||
""" | ||
|
||
idempotency_handler = IdempotencyHandler(handler, event, context, persistence_store) | ||
idempotency_handler = IdempotencyHandler(handler, event, context, config or IdempotencyConfig(), persistence_store) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to make refactoring easier later, could you please (I can do too): Use nitpick: kwargs over args for refactoring too |
||
|
||
# IdempotencyInconsistentStateError can happen under rare but expected cases when persistent state changes in the | ||
# small time between put & get requests. In most cases we can retry successfully on this exception. | ||
|
@@ -82,6 +89,7 @@ def __init__( | |
lambda_handler: Callable[[Any, LambdaContext], Any], | ||
event: Dict[str, Any], | ||
context: LambdaContext, | ||
config: IdempotencyConfig, | ||
persistence_store: BasePersistenceLayer, | ||
): | ||
""" | ||
|
@@ -98,6 +106,7 @@ def __init__( | |
persistence_store : BasePersistenceLayer | ||
Instance of persistence layer to store idempotency records | ||
""" | ||
persistence_store.configure(config) | ||
michaelbrewer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.persistence_store = persistence_store | ||
self.context = context | ||
self.event = event | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,14 @@ | |
import warnings | ||
from abc import ABC, abstractmethod | ||
from types import MappingProxyType | ||
from typing import Any, Dict | ||
from typing import Any, Dict, Optional | ||
|
||
import jmespath | ||
|
||
from aws_lambda_powertools.shared.cache_dict import LRUDict | ||
from aws_lambda_powertools.shared.jmespath_functions 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 ( | ||
IdempotencyInvalidStatusError, | ||
IdempotencyItemAlreadyExistsError, | ||
|
@@ -107,55 +108,49 @@ class BasePersistenceLayer(ABC): | |
Abstract Base Class for Idempotency persistence layer. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
event_key_jmespath: str = "", | ||
payload_validation_jmespath: str = "", | ||
expires_after_seconds: int = 60 * 60, # 1 hour default | ||
use_local_cache: bool = False, | ||
local_cache_max_items: int = 256, | ||
hash_function: str = "md5", | ||
raise_on_no_idempotency_key: bool = False, | ||
jmespath_options: Dict = None, | ||
) -> None: | ||
def __init__(self): | ||
"""Initialize the defaults """ | ||
self.configured = False | ||
self.event_key_jmespath: Optional[str] = None | ||
self.event_key_compiled_jmespath = None | ||
self.jmespath_options: Optional[dict] = None | ||
self.payload_validation_enabled = False | ||
self.validation_key_jmespath = None | ||
self.raise_on_no_idempotency_key = False | ||
self.expires_after_seconds: int = 60 * 60 # 1 hour default | ||
self.use_local_cache = False | ||
self._cache: Optional[LRUDict] = None | ||
self.hash_function = None | ||
|
||
def configure(self, config: IdempotencyConfig) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking of having this in the constructor and change the way we call it within IdempotencyHandler, but honestly that can be done later - there are a few areas we'll need to refactor, and this will be an internal change either way, and in the worst case we're calling it Beta too, it'll be a minor change IF that. |
||
""" | ||
Initialize the base persistence layer | ||
Initialize the base persistence layer from the configuration settings | ||
|
||
Parameters | ||
---------- | ||
event_key_jmespath: str | ||
A jmespath expression to extract the idempotency key from the event record | ||
payload_validation_jmespath: str | ||
A jmespath expression to extract the payload to be validated from the event record | ||
expires_after_seconds: int | ||
The number of seconds to wait before a record is expired | ||
use_local_cache: bool, optional | ||
Whether to locally cache idempotency results, by default False | ||
local_cache_max_items: int, optional | ||
Max number of items to store in local cache, by default 1024 | ||
hash_function: str, optional | ||
Function to use for calculating hashes, by default md5. | ||
raise_on_no_idempotency_key: bool, optional | ||
Raise exception if no idempotency key was found in the request, by default False | ||
jmespath_options : Dict | ||
Alternative JMESPath options to be included when filtering expr | ||
""" | ||
self.event_key_jmespath = event_key_jmespath | ||
if self.event_key_jmespath: | ||
self.event_key_compiled_jmespath = jmespath.compile(event_key_jmespath) | ||
self.expires_after_seconds = expires_after_seconds | ||
self.use_local_cache = use_local_cache | ||
if self.use_local_cache: | ||
self._cache = LRUDict(max_items=local_cache_max_items) | ||
self.payload_validation_enabled = False | ||
if payload_validation_jmespath: | ||
self.validation_key_jmespath = jmespath.compile(payload_validation_jmespath) | ||
config: IdempotencyConfig | ||
Idempotency configuration settings | ||
""" | ||
if self.configured: | ||
# Prevent being reconfigured multiple times | ||
return | ||
self.configured = True | ||
|
||
self.event_key_jmespath = config.event_key_jmespath | ||
if config.event_key_jmespath: | ||
self.event_key_compiled_jmespath = jmespath.compile(config.event_key_jmespath) | ||
self.jmespath_options = config.jmespath_options | ||
if not self.jmespath_options: | ||
self.jmespath_options = {"custom_functions": PowertoolsFunctions()} | ||
if config.payload_validation_jmespath: | ||
self.validation_key_jmespath = jmespath.compile(config.payload_validation_jmespath) | ||
self.payload_validation_enabled = True | ||
self.hash_function = getattr(hashlib, hash_function) | ||
self.raise_on_no_idempotency_key = raise_on_no_idempotency_key | ||
if not jmespath_options: | ||
jmespath_options = {"custom_functions": PowertoolsFunctions()} | ||
self.jmespath_options = jmespath_options | ||
self.raise_on_no_idempotency_key = config.raise_on_no_idempotency_key | ||
self.expires_after_seconds = config.expires_after_seconds | ||
self.use_local_cache = config.use_local_cache | ||
if self.use_local_cache: | ||
self._cache = LRUDict(max_items=config.local_cache_max_items) | ||
self.hash_function = getattr(hashlib, config.hash_function) | ||
|
||
def _get_hashed_idempotency_key(self, lambda_event: Dict[str, Any]) -> str: | ||
""" | ||
|
@@ -180,9 +175,9 @@ def _get_hashed_idempotency_key(self, lambda_event: Dict[str, Any]) -> str: | |
) | ||
|
||
if self.is_missing_idempotency_key(data): | ||
warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}") | ||
if self.raise_on_no_idempotency_key: | ||
raise IdempotencyKeyError("No data found to create a hashed idempotency_key") | ||
warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}") | ||
|
||
return self._generate_hash(data) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯