Skip to content

Commit 988dcfa

Browse files
authored
Merge pull request #2 from aradyaron/feat/idempotency-output-serializer-with-model
Feat/idempotency output serializer with model
2 parents 8b7d526 + fe4ff3d commit 988dcfa

File tree

6 files changed

+291
-12
lines changed

6 files changed

+291
-12
lines changed

aws_lambda_powertools/utilities/idempotency/exceptions.py

+12
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,15 @@ class IdempotencyKeyError(BaseError):
7171
"""
7272
Payload does not contain an idempotent key
7373
"""
74+
75+
76+
class IdempotencyModelTypeError(BaseError):
77+
"""
78+
Model type does not match expected type
79+
"""
80+
81+
82+
class IdempotencyNoSerializationModelError(BaseError):
83+
"""
84+
No model was supplied to the serializer
85+
"""

aws_lambda_powertools/utilities/idempotency/idempotency.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import functools
55
import logging
66
import os
7-
from typing import Any, Callable, Dict, Optional, cast
7+
from inspect import isclass
8+
from typing import Any, Callable, Dict, Optional, Type, Union, cast
89

910
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
1011
from aws_lambda_powertools.shared import constants
@@ -14,7 +15,10 @@
1415
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
1516
BasePersistenceLayer,
1617
)
17-
from aws_lambda_powertools.utilities.idempotency.serialization.base import BaseIdempotencySerializer
18+
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
19+
BaseIdempotencyModelSerializer,
20+
BaseIdempotencySerializer,
21+
)
1822
from aws_lambda_powertools.utilities.typing import LambdaContext
1923

2024
logger = logging.getLogger(__name__)
@@ -86,7 +90,7 @@ def idempotent_function(
8690
data_keyword_argument: str,
8791
persistence_store: BasePersistenceLayer,
8892
config: Optional[IdempotencyConfig] = None,
89-
output_serializer: Optional[BaseIdempotencySerializer] = None,
93+
output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]] = None,
9094
) -> Any:
9195
"""
9296
Decorator to handle idempotency of any function
@@ -101,9 +105,11 @@ def idempotent_function(
101105
Instance of BasePersistenceLayer to store data
102106
config: IdempotencyConfig
103107
Configuration
104-
output_serializer: Optional[BaseIdempotencySerializer]
108+
output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]]
105109
Serializer to transform the data to and from a dictionary.
106-
If not supplied, no serialization is done via the NoOpSerializer
110+
If not supplied, no serialization is done via the NoOpSerializer.
111+
In case a serializer of type inheriting BaseIdempotencyModelSerializer] is given,
112+
the serializer is deduced from the function return type.
107113
Examples
108114
--------
109115
**Processes an order in an idempotent manner**
@@ -131,6 +137,9 @@ def process_order(customer_id: str, order: dict, **kwargs):
131137
output_serializer=output_serializer,
132138
),
133139
)
140+
if isclass(output_serializer) and issubclass(output_serializer, BaseIdempotencyModelSerializer):
141+
# instantiate an instance of the serializer class
142+
output_serializer = output_serializer.instantiate(function.__annotations__.get("return", None))
134143

135144
config = config or IdempotencyConfig()
136145

aws_lambda_powertools/utilities/idempotency/serialization/base.py

+28
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,31 @@ def to_dict(self, data: Any) -> Dict:
1717
@abstractmethod
1818
def from_dict(self, data: Dict) -> Any:
1919
pass
20+
21+
22+
class BaseIdempotencyModelSerializer(BaseIdempotencySerializer):
23+
"""
24+
Abstract Base Class for Idempotency serialization layer, for using a model as data object representation.
25+
"""
26+
27+
@classmethod
28+
@abstractmethod
29+
def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer:
30+
"""
31+
Instantiate the serializer from the given model type.
32+
In case there is the model_type is unknown, None will be sent to the method.
33+
It's on the implementer to verify that:
34+
- None is handled
35+
- A model type not matching the expected types is handled
36+
37+
Parameters
38+
----------
39+
model_type: Any
40+
The model type to instantiate the class for
41+
42+
Returns
43+
-------
44+
BaseIdempotencySerializer
45+
Instance of the serializer class
46+
"""
47+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from dataclasses import asdict, is_dataclass
2+
from typing import Any, Dict, Type
3+
4+
from aws_lambda_powertools.utilities.idempotency.exceptions import (
5+
IdempotencyModelTypeError,
6+
IdempotencyNoSerializationModelError,
7+
)
8+
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
9+
BaseIdempotencyModelSerializer,
10+
BaseIdempotencySerializer,
11+
)
12+
13+
DataClass = Any
14+
15+
16+
class DataclassSerializer(BaseIdempotencyModelSerializer):
17+
def __init__(self, model: Type[DataClass]):
18+
"""
19+
Parameters
20+
----------
21+
model: Model
22+
A Pydantic model of the type to transform
23+
"""
24+
self.__model: Type[DataClass] = model
25+
26+
def to_dict(self, data: DataClass) -> Dict:
27+
return asdict(data)
28+
29+
def from_dict(self, data: Dict) -> DataClass:
30+
return self.__model(**data)
31+
32+
@classmethod
33+
def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer:
34+
if model_type is None:
35+
raise IdempotencyNoSerializationModelError("No serialization model was supplied")
36+
37+
if not is_dataclass(model_type):
38+
raise IdempotencyModelTypeError("Model type is not inherited of dataclass type")
39+
return cls(model=model_type)

aws_lambda_powertools/utilities/idempotency/serialization/pydantic.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
from typing import Dict, Type
1+
from typing import Any, Dict, Type
22

33
from pydantic import BaseModel
44

5-
from aws_lambda_powertools.utilities.idempotency.serialization.base import BaseIdempotencySerializer
5+
from aws_lambda_powertools.utilities.idempotency.exceptions import (
6+
IdempotencyModelTypeError,
7+
IdempotencyNoSerializationModelError,
8+
)
9+
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
10+
BaseIdempotencyModelSerializer,
11+
BaseIdempotencySerializer,
12+
)
613

714
Model = BaseModel
815

916

10-
class PydanticSerializer(BaseIdempotencySerializer):
17+
class PydanticSerializer(BaseIdempotencyModelSerializer):
1118
def __init__(self, model: Type[Model]):
1219
"""
1320
Parameters
@@ -28,3 +35,13 @@ def from_dict(self, data: Dict) -> Model:
2835
# Support for pydantic V2
2936
return self.__model.model_validate(data) # type: ignore[unused-ignore,attr-defined]
3037
return self.__model.parse_obj(data)
38+
39+
@classmethod
40+
def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer:
41+
if model_type is None:
42+
raise IdempotencyNoSerializationModelError("No serialization model was supplied")
43+
44+
if not issubclass(model_type, BaseModel):
45+
raise IdempotencyModelTypeError("Model type is not inherited from pydantic BaseModel")
46+
47+
return cls(model=model_type)

0 commit comments

Comments
 (0)