Skip to content

Commit 2981ec2

Browse files
committed
fix(docs): Extract idempotency code examples
Changes: - Extract code examples - Run isort and black - Fix yaml and python syntax - Add make task Related to: - aws-powertools#1064
1 parent b577366 commit 2981ec2

25 files changed

+549
-522
lines changed

Diff for: Makefile

+8
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,11 @@ changelog:
9090

9191
mypy:
9292
poetry run mypy --pretty aws_lambda_powertools
93+
94+
format-examples:
95+
poetry run isort docs/examples
96+
poetry run black docs/examples/*/*/*.py
97+
98+
lint-examples:
99+
poetry run python3 -m py_compile docs/examples/*/*/*.py
100+
cfn-lint docs/examples/*/*/*.yml

Diff for: docs/examples/utilities/idempotency/batch_sample.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from aws_lambda_powertools.utilities.batch import BatchProcessor, EventType, batch_processor
2+
from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord
3+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent_function
4+
5+
processor = BatchProcessor(event_type=EventType.SQS)
6+
dynamodb = DynamoDBPersistenceLayer(table_name="idem")
7+
config = IdempotencyConfig(
8+
event_key_jmespath="messageId", # see Choosing a payload subset section
9+
use_local_cache=True,
10+
)
11+
12+
13+
@idempotent_function(data_keyword_argument="record", config=config, persistence_store=dynamodb)
14+
def record_handler(record: SQSRecord):
15+
return {"message": record["body"]}
16+
17+
18+
@idempotent_function(data_keyword_argument="data", config=config, persistence_store=dynamodb)
19+
def dummy(arg_one, arg_two, data: dict, **kwargs):
20+
return {"data": data}
21+
22+
23+
@batch_processor(record_handler=record_handler, processor=processor)
24+
def lambda_handler(event, context):
25+
# `data` parameter must be called as a keyword argument to work
26+
dummy("hello", "universe", data="test")
27+
return processor.response()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import datetime
2+
import logging
3+
from typing import Any, Dict, Optional
4+
5+
import boto3
6+
from botocore.config import Config
7+
8+
from aws_lambda_powertools.utilities.idempotency import BasePersistenceLayer
9+
from aws_lambda_powertools.utilities.idempotency.exceptions import (
10+
IdempotencyItemAlreadyExistsError,
11+
IdempotencyItemNotFoundError,
12+
)
13+
from aws_lambda_powertools.utilities.idempotency.persistence.base import DataRecord
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class DynamoDBPersistenceLayer(BasePersistenceLayer):
19+
def __init__(
20+
self,
21+
table_name: str,
22+
key_attr: str = "id",
23+
expiry_attr: str = "expiration",
24+
status_attr: str = "status",
25+
data_attr: str = "data",
26+
validation_key_attr: str = "validation",
27+
boto_config: Optional[Config] = None,
28+
boto3_session: Optional[boto3.session.Session] = None,
29+
):
30+
boto_config = boto_config or Config()
31+
session = boto3_session or boto3.session.Session()
32+
self._ddb_resource = session.resource("dynamodb", config=boto_config)
33+
self.table_name = table_name
34+
self.table = self._ddb_resource.Table(self.table_name)
35+
self.key_attr = key_attr
36+
self.expiry_attr = expiry_attr
37+
self.status_attr = status_attr
38+
self.data_attr = data_attr
39+
self.validation_key_attr = validation_key_attr
40+
super(DynamoDBPersistenceLayer, self).__init__()
41+
42+
def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
43+
"""
44+
Translate raw item records from DynamoDB to DataRecord
45+
46+
Parameters
47+
----------
48+
item: Dict[str, Union[str, int]]
49+
Item format from dynamodb response
50+
51+
Returns
52+
-------
53+
DataRecord
54+
representation of item
55+
56+
"""
57+
return DataRecord(
58+
idempotency_key=item[self.key_attr],
59+
status=item[self.status_attr],
60+
expiry_timestamp=item[self.expiry_attr],
61+
response_data=item.get(self.data_attr),
62+
payload_hash=item.get(self.validation_key_attr),
63+
)
64+
65+
def _get_record(self, idempotency_key) -> DataRecord:
66+
response = self.table.get_item(Key={self.key_attr: idempotency_key}, ConsistentRead=True)
67+
68+
try:
69+
item = response["Item"]
70+
except KeyError:
71+
raise IdempotencyItemNotFoundError
72+
return self._item_to_data_record(item)
73+
74+
def _put_record(self, data_record: DataRecord) -> None:
75+
item = {
76+
self.key_attr: data_record.idempotency_key,
77+
self.expiry_attr: data_record.expiry_timestamp,
78+
self.status_attr: data_record.status,
79+
}
80+
81+
if self.payload_validation_enabled:
82+
item[self.validation_key_attr] = data_record.payload_hash
83+
84+
now = datetime.datetime.now()
85+
try:
86+
logger.debug(f"Putting record for idempotency key: {data_record.idempotency_key}")
87+
self.table.put_item(
88+
Item=item,
89+
ConditionExpression=f"attribute_not_exists({self.key_attr}) OR {self.expiry_attr} < :now",
90+
ExpressionAttributeValues={":now": int(now.timestamp())},
91+
)
92+
except self._ddb_resource.meta.client.exceptions.ConditionalCheckFailedException:
93+
logger.debug(f"Failed to put record for already existing idempotency key: {data_record.idempotency_key}")
94+
raise IdempotencyItemAlreadyExistsError
95+
96+
def _update_record(self, data_record: DataRecord):
97+
logger.debug(f"Updating record for idempotency key: {data_record.idempotency_key}")
98+
update_expression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"
99+
expression_attr_values = {
100+
":expiry": data_record.expiry_timestamp,
101+
":response_data": data_record.response_data,
102+
":status": data_record.status,
103+
}
104+
expression_attr_names = {
105+
"#response_data": self.data_attr,
106+
"#expiry": self.expiry_attr,
107+
"#status": self.status_attr,
108+
}
109+
110+
if self.payload_validation_enabled:
111+
update_expression += ", #validation_key = :validation_key"
112+
expression_attr_values[":validation_key"] = data_record.payload_hash
113+
expression_attr_names["#validation_key"] = self.validation_key_attr
114+
115+
kwargs = {
116+
"Key": {self.key_attr: data_record.idempotency_key},
117+
"UpdateExpression": update_expression,
118+
"ExpressionAttributeValues": expression_attr_values,
119+
"ExpressionAttributeNames": expression_attr_names,
120+
}
121+
122+
self.table.update_item(**kwargs)
123+
124+
def _delete_record(self, data_record: DataRecord) -> None:
125+
logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}")
126+
self.table.delete_item(Key={self.key_attr: data_record.idempotency_key})
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from dataclasses import dataclass
2+
3+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent_function
4+
5+
dynamodb = DynamoDBPersistenceLayer(table_name="idem")
6+
config = IdempotencyConfig(
7+
event_key_jmespath="order_id", # see Choosing a payload subset section
8+
use_local_cache=True,
9+
)
10+
11+
12+
@dataclass
13+
class OrderItem:
14+
sku: str
15+
description: str
16+
17+
18+
@dataclass
19+
class Order:
20+
item: OrderItem
21+
order_id: int
22+
23+
24+
@idempotent_function(data_keyword_argument="order", config=config, persistence_store=dynamodb)
25+
def process_order(order: Order):
26+
return f"processed order {order.order_id}"
27+
28+
29+
order_item = OrderItem(sku="fake", description="sample")
30+
order = Order(item=order_item, order_id="fake-id")
31+
32+
# `order` parameter must be called as a keyword argument to work
33+
process_order(order=order)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer
2+
3+
persistence_layer = DynamoDBPersistenceLayer(
4+
table_name="IdempotencyTable",
5+
key_attr="idempotency_key",
6+
expiry_attr="expires_at",
7+
status_attr="current_status",
8+
data_attr="result_data",
9+
validation_key_attr="validation_key",
10+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
2+
3+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
4+
config = IdempotencyConfig(
5+
event_key_jmespath="body",
6+
expires_after_seconds=5 * 60, # 5 minutes
7+
)
8+
9+
10+
@idempotent(config=config, persistence_store=persistence_layer)
11+
def handler(event, context):
12+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent
2+
3+
persistence_layer = DynamoDBPersistenceLayer(
4+
table_name="IdempotencyTable",
5+
sort_key_attr="sort_key",
6+
)
7+
8+
9+
@idempotent(persistence_store=persistence_layer)
10+
def handler(event, context):
11+
return {"message": "success", "id": event["body"]["id"]}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from botocore.config import Config
2+
3+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
4+
5+
config = IdempotencyConfig(event_key_jmespath="body")
6+
boto_config = Config()
7+
persistence_layer = DynamoDBPersistenceLayer(
8+
table_name="IdempotencyTable",
9+
boto_config=boto_config,
10+
)
11+
12+
13+
@idempotent(config=config, persistence_store=persistence_layer)
14+
def handler(event, context):
15+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import boto3
2+
3+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
4+
5+
boto3_session = boto3.session.Session()
6+
persistence_layer = DynamoDBPersistenceLayer(
7+
table_name="IdempotencyTable",
8+
boto3_session=boto3_session,
9+
)
10+
11+
config = IdempotencyConfig(event_key_jmespath="body")
12+
13+
14+
@idempotent(config=config, persistence_store=persistence_layer)
15+
def handler(event, context):
16+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import requests
2+
3+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent_function
4+
5+
dynamodb = DynamoDBPersistenceLayer(table_name="idem")
6+
config = IdempotencyConfig(event_key_jmespath="order_id")
7+
8+
9+
def lambda_handler(event, context):
10+
# If an exception is raised here, no idempotent record will ever get created as the
11+
# idempotent function does not get called
12+
do_some_stuff()
13+
14+
result = call_external_service(data={"user": "user1", "id": 5})
15+
16+
# This exception will not cause the idempotent record to be deleted, since it
17+
# happens after the decorated function has been successfully called
18+
raise Exception
19+
20+
21+
@idempotent_function(data_keyword_argument="data", config=config, persistence_store=dynamodb)
22+
def call_external_service(data: dict, **kwargs):
23+
result = requests.post("http://example.com", json={"user": data["user"], "transaction_id": data["id"]})
24+
return result.json()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
2+
3+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
4+
config = IdempotencyConfig(
5+
event_key_jmespath="body",
6+
use_local_cache=True,
7+
)
8+
9+
10+
@idempotent(config=config, persistence_store=persistence_layer)
11+
def handler(event, context):
12+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
2+
3+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
4+
5+
# Requires "user"."uid" and "order_id" to be present
6+
config = IdempotencyConfig(
7+
event_key_jmespath="[user.uid, order_id]",
8+
raise_on_no_idempotency_key=True,
9+
)
10+
11+
12+
@idempotent(config=config, persistence_store=persistence_layer)
13+
def handler(event, context):
14+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
2+
3+
config = IdempotencyConfig(
4+
event_key_jmespath="[userDetail, productId]",
5+
payload_validation_jmespath="amount",
6+
)
7+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
8+
9+
10+
@idempotent(config=config, persistence_store=persistence_layer)
11+
def handler(event, context):
12+
# Creating a subscription payment is a side
13+
# effect of calling this function!
14+
payment = create_subscription_payment(
15+
user=event["userDetail"]["username"],
16+
product=event["product_id"],
17+
amount=event["amount"],
18+
)
19+
...
20+
return {
21+
"message": "success",
22+
"statusCode": 200,
23+
"payment_id": payment.id,
24+
"amount": payment.amount,
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent
2+
from aws_lambda_powertools.utilities.validation import envelopes, validator
3+
4+
config = IdempotencyConfig(event_key_jmespath="[message, username]")
5+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
6+
7+
8+
@validator(envelope=envelopes.API_GATEWAY_HTTP)
9+
@idempotent(config=config, persistence_store=persistence_layer)
10+
def lambda_handler(event, context):
11+
cause_some_side_effects(event["username"])
12+
return {"message": event["message"], "statusCode": 200}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent
2+
3+
persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable")
4+
5+
6+
@idempotent(persistence_store=persistence_layer)
7+
def handler(event, context):
8+
payment = create_subscription_payment(user=event["user"], product=event["product_id"])
9+
...
10+
return {
11+
"payment_id": payment.id,
12+
"message": "success",
13+
"statusCode": 200,
14+
}

0 commit comments

Comments
 (0)