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

refactor(idempotency): add from __future__ import annotations #4961

Merged
merged 2 commits into from
Aug 15, 2024
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
48 changes: 26 additions & 22 deletions aws_lambda_powertools/utilities/idempotency/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from __future__ import annotations

import datetime
import logging
from copy import deepcopy
from typing import Any, Callable, Dict, Optional, Tuple
from typing import TYPE_CHECKING, Any, Callable

from aws_lambda_powertools.utilities.idempotency.config import (
IdempotencyConfig,
)
from aws_lambda_powertools.utilities.idempotency.exceptions import (
IdempotencyAlreadyInProgressError,
IdempotencyInconsistentStateError,
Expand All @@ -15,20 +14,25 @@
IdempotencyPersistenceLayerError,
IdempotencyValidationError,
)
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
BasePersistenceLayer,
)
from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import (
STATUS_CONSTANTS,
DataRecord,
)
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
BaseIdempotencySerializer,
)
from aws_lambda_powertools.utilities.idempotency.serialization.no_op import (
NoOpSerializer,
)

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.idempotency.config import (
IdempotencyConfig,
)
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
BasePersistenceLayer,
)
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
BaseIdempotencySerializer,
)

MAX_RETRIES = 2
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -69,9 +73,9 @@ def __init__(
function_payload: Any,
config: IdempotencyConfig,
persistence_store: BasePersistenceLayer,
output_serializer: Optional[BaseIdempotencySerializer] = None,
function_args: Optional[Tuple] = None,
function_kwargs: Optional[Dict] = None,
output_serializer: BaseIdempotencySerializer | None = None,
function_args: tuple | None = None,
function_kwargs: dict | None = None,
):
"""
Initialize the IdempotencyHandler
Expand All @@ -84,12 +88,12 @@ def __init__(
Idempotency Configuration
persistence_store : BasePersistenceLayer
Instance of persistence layer to store idempotency records
output_serializer: Optional[BaseIdempotencySerializer]
output_serializer: BaseIdempotencySerializer | None
Serializer to transform the data to and from a dictionary.
If not supplied, no serialization is done via the NoOpSerializer
function_args: Optional[Tuple]
function_args: tuple | None
Function arguments
function_kwargs: Optional[Dict]
function_kwargs: dict | None
Function keyword arguments
"""
self.function = function
Expand Down Expand Up @@ -150,7 +154,7 @@ def _process_idempotency(self):

return self._get_function_response()

def _get_remaining_time_in_millis(self) -> Optional[int]:
def _get_remaining_time_in_millis(self) -> int | None:
"""
Tries to determine the remaining time available for the current lambda invocation.

Expand All @@ -160,7 +164,7 @@ def _get_remaining_time_in_millis(self) -> Optional[int]:

Returns
-------
Optional[int]
int | None
Remaining time in millis, or None if the remaining time cannot be determined.
"""

Expand All @@ -169,7 +173,7 @@ def _get_remaining_time_in_millis(self) -> Optional[int]:

return None

def _get_idempotency_record(self) -> Optional[DataRecord]:
def _get_idempotency_record(self) -> DataRecord | None:
"""
Retrieve the idempotency record from the persistence layer.

Expand Down Expand Up @@ -198,7 +202,7 @@ def _get_idempotency_record(self) -> Optional[DataRecord]:

return data_record

def _handle_for_status(self, data_record: DataRecord) -> Optional[Any]:
def _handle_for_status(self, data_record: DataRecord) -> Any | None:
"""
Take appropriate action based on data_record's status

Expand All @@ -208,7 +212,7 @@ def _handle_for_status(self, data_record: DataRecord) -> Optional[Any]:

Returns
-------
Optional[Any]
Any | None
Function's response previously used for this idempotency key, if it has successfully executed already.
In case an output serializer is configured, the response is deserialized.

Expand All @@ -235,7 +239,7 @@ def _handle_for_status(self, data_record: DataRecord) -> Optional[Any]:
f"Execution already in progress with idempotency key: "
f"{self.persistence_store.event_key_jmespath}={data_record.idempotency_key}",
)
response_dict: Optional[dict] = data_record.response_json_as_dict()
response_dict: dict | None = 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:
Expand Down
19 changes: 11 additions & 8 deletions aws_lambda_powertools/utilities/idempotency/config.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
from typing import Dict, Optional
from __future__ import annotations

from aws_lambda_powertools.utilities.idempotency import IdempotentHookFunction
from aws_lambda_powertools.utilities.typing import LambdaContext
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.idempotency import IdempotentHookFunction
from aws_lambda_powertools.utilities.typing import LambdaContext


class IdempotencyConfig:
def __init__(
self,
event_key_jmespath: str = "",
payload_validation_jmespath: str = "",
jmespath_options: Optional[Dict] = None,
jmespath_options: dict | None = 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",
lambda_context: Optional[LambdaContext] = None,
response_hook: Optional[IdempotentHookFunction] = None,
lambda_context: LambdaContext | None = None,
response_hook: IdempotentHookFunction | None = None,
):
"""
Initialize the base persistence layer
Expand Down Expand Up @@ -50,8 +53,8 @@ def __init__(
self.use_local_cache = use_local_cache
self.local_cache_max_items = local_cache_max_items
self.hash_function = hash_function
self.lambda_context: Optional[LambdaContext] = lambda_context
self.response_hook: Optional[IdempotentHookFunction] = response_hook
self.lambda_context: LambdaContext | None = lambda_context
self.response_hook: IdempotentHookFunction | None = response_hook

def register_lambda_context(self, lambda_context: LambdaContext):
"""Captures the Lambda context, to calculate the remaining time before the invocation times out"""
Expand Down
11 changes: 7 additions & 4 deletions aws_lambda_powertools/utilities/idempotency/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
Idempotency errors
"""

from typing import Optional, Union
from __future__ import annotations

from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord


class BaseError(Exception):
Expand All @@ -13,7 +16,7 @@ class BaseError(Exception):
See https://github.com/aws-powertools/powertools-lambda-python/issues/1772
"""

def __init__(self, *args: Optional[Union[str, Exception]]):
def __init__(self, *args: str | Exception | None):
self.message = str(args[0]) if args else ""
self.details = "".join(str(arg) for arg in args[1:]) if args[1:] else None

Expand All @@ -31,7 +34,7 @@ class IdempotencyItemAlreadyExistsError(BaseError):
Item attempting to be inserted into persistence store already exists and is not expired
"""

def __init__(self, *args: Optional[Union[str, Exception]], old_data_record: Optional[DataRecord] = None):
def __init__(self, *args: str | Exception | None, old_data_record: DataRecord | None = None):
self.old_data_record = old_data_record
super().__init__(*args)

Expand Down
7 changes: 5 additions & 2 deletions aws_lambda_powertools/utilities/idempotency/hook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import Any, Protocol
from __future__ import annotations

from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord
from typing import TYPE_CHECKING, Any, Protocol

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.idempotency.persistence.datarecord import DataRecord


class IdempotentHookFunction(Protocol):
Expand Down
30 changes: 17 additions & 13 deletions aws_lambda_powertools/utilities/idempotency/idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,40 @@
Primary interface for idempotent Lambda functions utility
"""

from __future__ import annotations

import functools
import logging
import os
from inspect import isclass
from typing import Any, Callable, Dict, Optional, Type, Union, cast
from typing import TYPE_CHECKING, Any, Callable, cast

from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.types import AnyCallableT
from aws_lambda_powertools.utilities.idempotency.base import IdempotencyHandler
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
BasePersistenceLayer,
)
from aws_lambda_powertools.utilities.idempotency.serialization.base import (
BaseIdempotencyModelSerializer,
BaseIdempotencySerializer,
)
from aws_lambda_powertools.utilities.typing import LambdaContext

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.idempotency.persistence.base import (
BasePersistenceLayer,
)
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = logging.getLogger(__name__)


@lambda_handler_decorator
def idempotent(
handler: Callable[[Any, LambdaContext], Any],
event: Dict[str, Any],
event: dict[str, Any],
context: LambdaContext,
persistence_store: BasePersistenceLayer,
config: Optional[IdempotencyConfig] = None,
config: IdempotencyConfig | None = None,
**kwargs,
) -> Any:
"""
Expand All @@ -41,9 +45,9 @@ def idempotent(
----------
handler: Callable
Lambda's handler
event: Dict
event: dict
Lambda's Event
context: Dict
context: dict
Lambda's Context
persistence_store: BasePersistenceLayer
Instance of BasePersistenceLayer to store data
Expand Down Expand Up @@ -86,12 +90,12 @@ def idempotent(


def idempotent_function(
function: Optional[AnyCallableT] = None,
function: AnyCallableT | None = None,
*,
data_keyword_argument: str,
persistence_store: BasePersistenceLayer,
config: Optional[IdempotencyConfig] = None,
output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]] = None,
config: IdempotencyConfig | None = None,
output_serializer: BaseIdempotencySerializer | type[BaseIdempotencyModelSerializer] | None = None,
**kwargs: Any,
) -> Any:
"""
Expand All @@ -107,7 +111,7 @@ def idempotent_function(
Instance of BasePersistenceLayer to store data
config: IdempotencyConfig
Configuration
output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]]
output_serializer: BaseIdempotencySerializer | type[BaseIdempotencyModelSerializer] | None
Serializer to transform the data to and from a dictionary.
If not supplied, no serialization is done via the NoOpSerializer.
In case a serializer of type inheriting BaseIdempotencyModelSerializer is given,
Expand Down
Loading
Loading