From 0727da830039318fee83e5784635bbdce57b4d8a Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 14 May 2025 21:37:46 +0100 Subject: [PATCH 1/3] Adding cache name --- .../idempotency/persistence/redis.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index 7f27566cc24..7a11c85a113 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -8,6 +8,7 @@ from typing import Any, Literal, Protocol import redis +from typing_extensions import deprecated from aws_lambda_powertools.utilities.idempotency import BasePersistenceLayer from aws_lambda_powertools.utilities.idempotency.exceptions import ( @@ -25,11 +26,12 @@ logger = logging.getLogger(__name__) +@deprecated("RedisPersistenceLayer will be removed in v4.0.0. Please use CacheProtocol instead.") class RedisClientProtocol(Protocol): """ - Protocol class defining the interface for a Redis client. + Protocol class defining the interface for a Cache client. - This protocol outlines the expected behavior of a Redis client, allowing for + This protocol outlines the expected behavior of a Cache client, allowing for standardization among different implementations and allowing customers to extend it in their own implementation. @@ -78,6 +80,7 @@ def delete(self, keys: bytes | str | memoryview) -> Any: raise NotImplementedError +@deprecated("RedisConnection will be removed in v4.0.0. Please use CacheConnection instead.") class RedisConnection: def __init__( self, @@ -91,26 +94,26 @@ def __init__( ssl: bool = True, ) -> None: """ - Initialize Redis connection which will be used in Redis persistence_store to support Idempotency + Initialize Cache connection which will be used in Cache persistence_store to support Idempotency Parameters ---------- host: str, optional - Redis host + Cache host port: int, optional: default 6379 - Redis port + Cache port username: str, optional - Redis username + Cache username password: str, optional - Redis password + Cache password url: str, optional - Redis connection string, using url will override the host/port in the previous parameters + Cache connection string, using url will override the host/port in the previous parameters db_index: int, optional: default 0 - Redis db index + Cache db index mode: str, Literal["standalone","cluster"] - set Redis client mode, choose from standalone/cluster. The default is standalone + set Cache client mode, choose from standalone/cluster. The default is standalone ssl: bool, optional: default True - set whether to use ssl for Redis connection + set whether to use ssl for Cache connection Example -------- @@ -122,8 +125,8 @@ def __init__( from aws_lambda_powertools.utilities.idempotency import ( idempotent, ) - from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, + from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) from aws_lambda_powertools.utilities.typing import LambdaContext @@ -204,6 +207,7 @@ def _init_client(self) -> RedisClientProtocol: raise IdempotencyPersistenceConnectionError("Could not to connect to Redis", exc) from exc +@deprecated("RedisCachePersistenceLayer will be removed in v4.0.0. Please use CachePersistenceLayer instead.") class RedisCachePersistenceLayer(BasePersistenceLayer): def __init__( self, From 0a399ba434968e90ec610bdebe2f2e09a2b15e38 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 19 May 2025 17:41:13 +0100 Subject: [PATCH 2/3] Replacing Redis with Cache --- Makefile | 6 +- .../idempotency/persistence/cache.py | 11 +++ .../idempotency/persistence/redis.py | 99 ++++++++++--------- docs/utilities/idempotency.md | 49 ++++----- ..._started_with_idempotency_cache_config.py} | 10 +- ...g_started_with_idempotency_redis_client.py | 10 +- ..._started_with_idempotency_valkey_client.py | 53 ++++++++++ .../using_redis_client_with_aws_secrets.py | 34 ++++--- .../using_redis_client_with_local_certs.py | 6 +- .../templates/cfn_redis_serverless.yaml | 14 +-- poetry.lock | 80 ++++++++++++--- pyproject.toml | 2 + 12 files changed, 251 insertions(+), 123 deletions(-) create mode 100644 aws_lambda_powertools/utilities/idempotency/persistence/cache.py rename examples/idempotency/src/{getting_started_with_idempotency_redis_config.py => getting_started_with_idempotency_cache_config.py} (77%) create mode 100644 examples/idempotency/src/getting_started_with_idempotency_valkey_client.py diff --git a/Makefile b/Makefile index 843c8aab17e..0718cc52bee 100644 --- a/Makefile +++ b/Makefile @@ -7,18 +7,18 @@ target: dev: pip install --upgrade pip pre-commit poetry @$(MAKE) dev-version-plugin - poetry install --extras "all redis datamasking" + poetry install --extras "all redis datamasking valkey" pre-commit install dev-quality-code: pip install --upgrade pip pre-commit poetry @$(MAKE) dev-version-plugin - poetry install --extras "all redis datamasking" + poetry install --extras "all redis datamasking valkey" pre-commit install dev-gitpod: pip install --upgrade pip poetry - poetry install --extras "all redis datamasking" + poetry install --extras "all redis datamasking valkey" pre-commit install # Running licensecheck with zero to break the pipeline if there is an invalid license diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/cache.py b/aws_lambda_powertools/utilities/idempotency/persistence/cache.py new file mode 100644 index 00000000000..fcd2c37c7c1 --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/persistence/cache.py @@ -0,0 +1,11 @@ +from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( + CacheClientProtocol, + CacheConnection, + CachePersistenceLayer, +) + +__all__ = [ + "CacheClientProtocol", + "CachePersistenceLayer", + "CacheConnection", +] diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index 7a11c85a113..d1c490ee0f3 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -8,7 +8,7 @@ from typing import Any, Literal, Protocol import redis -from typing_extensions import deprecated +from typing_extensions import TypeAlias, deprecated from aws_lambda_powertools.utilities.idempotency import BasePersistenceLayer from aws_lambda_powertools.utilities.idempotency.exceptions import ( @@ -88,7 +88,7 @@ def __init__( host: str = "", port: int = 6379, username: str = "", - password: str = "", # nosec - password for Redis connection + password: str = "", # nosec - password for Cache connection db_index: int = 0, mode: Literal["standalone", "cluster"] = "standalone", ssl: bool = True, @@ -131,7 +131,7 @@ def __init__( from aws_lambda_powertools.utilities.typing import LambdaContext - persistence_layer = RedisCachePersistenceLayer(host="localhost", port=6379) + persistence_layer = CachePersistenceLayer(host="localhost", port=6379) @dataclass @@ -184,15 +184,15 @@ def _init_client(self) -> RedisClientProtocol: try: if self.url: - logger.debug(f"Using URL format to connect to Redis: {self.host}") + logger.debug(f"Using URL format to connect to Cache: {self.host}") return client.from_url(url=self.url) else: - # Redis in cluster mode doesn't support db parameter + # Cache in cluster mode doesn't support db parameter extra_param_connection: dict[str, Any] = {} if self.mode != "cluster": extra_param_connection = {"db": self.db_index} - logger.debug(f"Using arguments to connect to Redis: {self.host}") + logger.debug(f"Using arguments to connect to Cache: {self.host}") return client( host=self.host, port=self.port, @@ -203,8 +203,8 @@ def _init_client(self) -> RedisClientProtocol: **extra_param_connection, ) except redis.exceptions.ConnectionError as exc: - logger.debug(f"Cannot connect in Redis: {self.host}") - raise IdempotencyPersistenceConnectionError("Could not to connect to Redis", exc) from exc + logger.debug(f"Cannot connect to Cache endpoint: {self.host}") + raise IdempotencyPersistenceConnectionError("Could not to connect to Cache endpoint", exc) from exc @deprecated("RedisCachePersistenceLayer will be removed in v4.0.0. Please use CachePersistenceLayer instead.") @@ -215,7 +215,7 @@ def __init__( host: str = "", port: int = 6379, username: str = "", - password: str = "", # nosec - password for Redis connection + password: str = "", # nosec - password for Cache connection db_index: int = 0, mode: Literal["standalone", "cluster"] = "standalone", ssl: bool = True, @@ -227,39 +227,39 @@ def __init__( validation_key_attr: str = "validation", ): """ - Initialize the Redis Persistence Layer + Initialize the Cache Persistence Layer Parameters ---------- host: str, optional - Redis host + Cache host port: int, optional: default 6379 - Redis port + Cache port username: str, optional - Redis username + Cache username password: str, optional - Redis password + Cache password url: str, optional - Redis connection string, using url will override the host/port in the previous parameters + Cache connection string, using url will override the host/port in the previous parameters db_index: int, optional: default 0 - Redis db index + Cache db index mode: str, Literal["standalone","cluster"] - set Redis client mode, choose from standalone/cluster + set Cache client mode, choose from standalone/cluster ssl: bool, optional: default True - set whether to use ssl for Redis connection - client: RedisClientProtocol, optional - Bring your own Redis client that follows RedisClientProtocol. + set whether to use ssl for Cache connection + client: CacheClientProtocol, optional + Bring your own Cache client that follows CacheClientProtocol. If provided, all other connection configuration options will be ignored expiry_attr: str, optional - Redis json attribute name for expiry timestamp, by default "expiration" + Cache json attribute name for expiry timestamp, by default "expiration" in_progress_expiry_attr: str, optional - Redis json attribute name for in-progress expiry timestamp, by default "in_progress_expiration" + Cache json attribute name for in-progress expiry timestamp, by default "in_progress_expiration" status_attr: str, optional - Redis json attribute name for status, by default "status" + Cache json attribute name for status, by default "status" data_attr: str, optional - Redis json attribute name for response data, by default "data" + Cache json attribute name for response data, by default "data" validation_key_attr: str, optional - Redis json attribute name for hashed representation of the parts of the event used for validation + Cache json attribute name for hashed representation of the parts of the event used for validation Examples -------- @@ -270,8 +270,8 @@ def __init__( idempotent, ) - from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, + from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) client = redis.Redis( @@ -279,7 +279,7 @@ def __init__( port="6379", decode_responses=True, ) - persistence_layer = RedisCachePersistenceLayer(client=client) + persistence_layer = CachePersistenceLayer(client=client) @idempotent(persistence_store=persistence_layer) def lambda_handler(event: dict, context: LambdaContext): @@ -292,7 +292,7 @@ def lambda_handler(event: dict, context: LambdaContext): ``` """ - # Initialize Redis client with Redis config if no client is passed in + # Initialize Cache client with cache config if no client is passed in if client is None: self.client = RedisConnection( host=host, @@ -334,11 +334,11 @@ def _item_to_data_record(self, idempotency_key: str, item: dict[str, Any]) -> Da in_progress_expiry_timestamp=in_progress_expiry_timestamp, response_data=str(item.get(self.data_attr)), payload_hash=str(item.get(self.validation_key_attr)), - expiry_timestamp=item.get("expiration", None), + expiry_timestamp=item.get("expiration"), ) def _get_record(self, idempotency_key) -> DataRecord: - # See: https://redis.io/commands/get/ + # See: https://valkey.io/valkey-glide/python/core/#glide.async_commands.CoreCommands.set response = self.client.get(idempotency_key) # key not found @@ -388,25 +388,25 @@ def _put_in_progress_record(self, data_record: DataRecord) -> None: # The idempotency key does not exist: # - first time that this invocation key is used # - previous invocation with the same key was deleted due to TTL - # - SET see https://redis.io/commands/set/ + # - SET see https://valkey.io/valkey-glide/python/core/#glide.async_commands.CoreCommands.set - logger.debug(f"Putting record on Redis for idempotency key: {data_record.idempotency_key}") + logger.debug(f"Putting record on Cache for idempotency key: {data_record.idempotency_key}") encoded_item = self._json_serializer(item["mapping"]) ttl = self._get_expiry_second(expiry_timestamp=data_record.expiry_timestamp) - redis_response = self.client.set(name=data_record.idempotency_key, value=encoded_item, ex=ttl, nx=True) + cache_response = self.client.set(name=data_record.idempotency_key, value=encoded_item, ex=ttl, nx=True) - # If redis_response is True, the Redis SET operation was successful and the idempotency key was not + # If cache_response is True, the Cache SET operation was successful and the idempotency key was not # previously set. This indicates that we can safely proceed to the handler execution phase. # Most invocations should successfully proceed past this point. - if redis_response: + if cache_response: return - # If redis_response is None, it indicates an existing record in Redis for the given idempotency key. + # If cache_response is None, it indicates an existing record in Cache for the given idempotency key. # This could be due to: # - An active idempotency record from a previous invocation that has not yet expired. # - An orphan record where a previous invocation has timed out. - # - An expired idempotency record that has not been deleted by Redis. + # - An expired idempotency record that has not been deleted by Cache. # In any case, we proceed to retrieve the record for further inspection. idempotency_record = self._get_record(data_record.idempotency_key) @@ -431,7 +431,7 @@ def _put_in_progress_record(self, data_record: DataRecord) -> None: # Reaching this point indicates that the idempotency record found is an orphan record. An orphan record is # one that is neither completed nor in-progress within its expected time frame. It may result from a - # previous invocation that has timed out or an expired record that has yet to be cleaned up by Redis. + # previous invocation that has timed out or an expired record that has yet to be cleaned up by Cache. # We raise an error to handle this exceptional scenario appropriately. raise IdempotencyPersistenceConsistencyError @@ -439,24 +439,22 @@ def _put_in_progress_record(self, data_record: DataRecord) -> None: # Handle an orphan record by attempting to acquire a lock, which by default lasts for 10 seconds. # The purpose of acquiring the lock is to prevent race conditions with other processes that might # also be trying to handle the same orphan record. Once the lock is acquired, we set a new value - # for the idempotency record in Redis with the appropriate time-to-live (TTL). + # for the idempotency record in Cache with the appropriate time-to-live (TTL). with self._acquire_lock(name=item["name"]): self.client.set(name=item["name"], value=encoded_item, ex=ttl) # Not removing the lock here serves as a safeguard against race conditions, # preventing another operation from mistakenly treating this record as an orphan while the # current operation is still in progress. - except (redis.exceptions.RedisError, redis.exceptions.RedisClusterException) as e: - raise e except Exception as e: - logger.debug(f"encountered non-Redis exception: {e}") - raise e + logger.debug(f"An error occurred: {e}") + raise @contextmanager def _acquire_lock(self, name: str): """ Attempt to acquire a lock for a specified resource name, with a default timeout. - This context manager attempts to set a lock using Redis to prevent concurrent + This context manager attempts to set a lock using Cache to prevent concurrent access to a resource identified by 'name'. It uses the 'nx' flag to ensure that the lock is only set if it does not already exist, thereby enforcing mutual exclusion. """ @@ -500,9 +498,9 @@ def _update_record(self, data_record: DataRecord) -> None: def _delete_record(self, data_record: DataRecord) -> None: """ - Deletes the idempotency record associated with a given DataRecord from Redis. + Deletes the idempotency record associated with a given DataRecord from Cache. This function is designed to be called after a Lambda handler invocation has completed processing. - It ensures that the idempotency key associated with the DataRecord is removed from Redis to + It ensures that the idempotency key associated with the DataRecord is removed from Cache to prevent future conflicts and to maintain the idempotency integrity. Note: it is essential that the idempotency key is not empty, as that would indicate the Lambda @@ -510,5 +508,10 @@ def _delete_record(self, data_record: DataRecord) -> None: """ logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}") - # See: https://redis.io/commands/del/ + # See: https://valkey.io/valkey-glide/python/core/#glide.async_commands.CoreCommands.delete self.client.delete(data_record.idempotency_key) + + +CachePersistenceLayer: TypeAlias = RedisCachePersistenceLayer +CacheClientProtocol: TypeAlias = RedisClientProtocol +CacheConnection: TypeAlias = RedisConnection diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 97ffd38903b..5ab38ae5ea2 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -12,7 +12,7 @@ The idempotency utility allows you to retry operations within a time window with * Produces the previous successful result when a function is called repeatedly with the same idempotency key * Choose your idempotency key from one or more fields, or entire payload * Safeguard concurrent requests, timeouts, missing idempotency keys, and payload tampering -* Support for Amazon DynamoDB, Redis, bring your own persistence layer, and in-memory caching +* Support for Amazon DynamoDB, Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer ## Terminology @@ -82,7 +82,7 @@ To start, you'll need: --- - [Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database) + [Amazon DynamoDB](#dynamodb-table) or [Valkey/Redis OSS/Redis compatible](#cache-database) * :simple-awslambda:{ .lg .middle } **AWS Lambda function** @@ -139,13 +139,13 @@ You **can** use a single DynamoDB table for all functions annotated with Idempot * **Old boto3 versions can increase costs**. For cost optimization, we use a conditional `PutItem` to always lock a new idempotency record. If locking fails, it means we already have an idempotency record saving us an additional `GetItem` call. However, this is only supported in boto3 `1.26.194` and higher _([June 30th 2023](https://aws.amazon.com/about-aws/whats-new/2023/06/amazon-dynamodb-cost-failed-conditional-writes/){target="_blank"})_. -#### Redis database +#### Cache database -We recommend you start with a Redis compatible management services such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb/){target="_blank"}. +We recommend starting with a managed cache service, such as [Amazon ElastiCache for Valkey and for Redis OSS](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB](https://aws.amazon.com/memorydb/){target="_blank"}. In both services, you'll need to configure [VPC access](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html){target="_blank"} to your AWS Lambda. -##### Redis IaC examples +##### Cache configuration === "AWS CloudFormation example" @@ -160,7 +160,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon. 1. Replace the Security Group ID and Subnet ID to match your VPC settings. 2. Replace the Security Group ID and Subnet ID to match your VPC settings. -Once setup, you can find a quick start and advanced examples for Redis in [the persistent layers section](#redispersistencelayer). +Once setup, you can find a quick start and advanced examples for Cache in [the persistent layers section](#cachepersistencelayer). @@ -464,17 +464,22 @@ You can customize the attribute names during initialization: | **sort_key_attr** | | | Sort key of the table (if table is configured with a sort key). | | **static_pk_value** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **sort_key_attr** is set. | -#### RedisPersistenceLayer +#### CachePersistenceLayer -!!! info "We recommend Redis version 7 or higher for optimal performance." +The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state. -For simple setups, initialize `RedisCachePersistenceLayer` with your Redis endpoint and port to connect. +We recommend using [`valkey-glide`](https://pypi.org/project/valkey-glide/){target="_blank"} for Valkey or [`redis`](https://pypi.org/project/redis/){target="_blank"} for Redis. However, any Redis OSS-compatible client should work. -For security, we enforce SSL connections by default; to disable it, set `ssl=False`. +For simple setups, initialize `CachePersistenceLayer` with your Cache endpoint and port to connect. Note that for security, we enforce SSL connections by default; to disable it, set `ssl=False`. -=== "Redis quick start" - ```python title="getting_started_with_idempotency_redis_config.py" hl_lines="8-10 14 27" - --8<-- "examples/idempotency/src/getting_started_with_idempotency_redis_config.py" +=== "Cache quick start" + ```python title="getting_started_with_idempotency_cache_config.py" hl_lines="8-10 14 27" + --8<-- "examples/idempotency/src/getting_started_with_idempotency_cache_config.py" + ``` + +=== "Using an existing Valkey Glide client" + ```python title="getting_started_with_idempotency_valkey_client.py" hl_lines="5 10-12 16-22 24 37" + --8<-- "examples/idempotency/src/getting_started_with_idempotency_valkey_client.py" ``` === "Using an existing Redis client" @@ -488,11 +493,11 @@ For security, we enforce SSL connections by default; to disable it, set `ssl=Fal --8<-- "examples/idempotency/src/getting_started_with_idempotency_payload.json" ``` -##### Redis SSL connections +##### Cache SSL connections We recommend using AWS Secrets Manager to store and rotate certificates safely, and the [Parameters feature](./parameters.md){target="_blank"} to fetch and cache optimally. -For advanced configurations, we recommend using an existing Redis client for optimal compatibility like SSL certificates and timeout. +For advanced configurations, we recommend using an existing Valkey client for optimal compatibility like SSL certificates and timeout. === "Advanced configuration using AWS Secrets" ```python title="using_redis_client_with_aws_secrets.py" hl_lines="9-11 13 15 25" @@ -525,7 +530,7 @@ For advanced configurations, we recommend using an existing Redis client for opt 3. redis_user_private.key file stored in the "certs" directory of your Lambda function 4. redis_ca.pem file stored in the "certs" directory of your Lambda function -##### Redis attributes +##### Cache attributes You can customize the attribute names during initialization: @@ -811,28 +816,28 @@ sequenceDiagram Optional idempotency key -#### Race condition with Redis +#### Race condition with Cache
```mermaid graph TD; - A(Existing orphan record in redis)-->A1; + A(Existing orphan record in cache)-->A1; A1[Two Lambda invoke at same time]-->B1[Lambda handler1]; - B1-->B2[Fetch from Redis]; + B1-->B2[Fetch from Cache]; B2-->B3[Handler1 got orphan record]; B3-->B4[Handler1 acquired lock]; B4-->B5[Handler1 overwrite orphan record] B5-->B6[Handler1 continue to execution]; A1-->C1[Lambda handler2]; - C1-->C2[Fetch from Redis]; + C1-->C2[Fetch from Cache]; C2-->C3[Handler2 got orphan record]; C3-->C4[Handler2 failed to acquire lock]; - C4-->C5[Handler2 wait and fetch from Redis]; + C4-->C5[Handler2 wait and fetch from Cache]; C5-->C6[Handler2 return without executing]; B6-->D(Lambda handler executed only once); C6-->D; ``` -Race condition with Redis +Race condition with Cache
## Advanced diff --git a/examples/idempotency/src/getting_started_with_idempotency_redis_config.py b/examples/idempotency/src/getting_started_with_idempotency_cache_config.py similarity index 77% rename from examples/idempotency/src/getting_started_with_idempotency_redis_config.py rename to examples/idempotency/src/getting_started_with_idempotency_cache_config.py index f3917042b28..5425bbee9d3 100644 --- a/examples/idempotency/src/getting_started_with_idempotency_redis_config.py +++ b/examples/idempotency/src/getting_started_with_idempotency_cache_config.py @@ -5,13 +5,13 @@ from aws_lambda_powertools.utilities.idempotency import ( idempotent, ) -from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, +from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) from aws_lambda_powertools.utilities.typing import LambdaContext -redis_endpoint = os.getenv("REDIS_CLUSTER_ENDPOINT", "localhost") -persistence_layer = RedisCachePersistenceLayer(host=redis_endpoint, port=6379) +redis_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost") +persistence_layer = CachePersistenceLayer(host=redis_endpoint, port=6379) @dataclass @@ -34,7 +34,7 @@ def lambda_handler(event: dict, context: LambdaContext): "statusCode": 200, } except Exception as exc: - raise PaymentError(f"Error creating payment {str(exc)}") + raise PaymentError(f"Error creating payment {str(exc)}") from exc def create_subscription_payment(event: dict) -> Payment: diff --git a/examples/idempotency/src/getting_started_with_idempotency_redis_client.py b/examples/idempotency/src/getting_started_with_idempotency_redis_client.py index 24dfe1be117..ac2a20587e8 100644 --- a/examples/idempotency/src/getting_started_with_idempotency_redis_client.py +++ b/examples/idempotency/src/getting_started_with_idempotency_redis_client.py @@ -7,21 +7,21 @@ from aws_lambda_powertools.utilities.idempotency import ( idempotent, ) -from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, +from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) from aws_lambda_powertools.utilities.typing import LambdaContext -redis_endpoint = os.getenv("REDIS_CLUSTER_ENDPOINT", "localhost") +cache_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost") client = Redis( - host=redis_endpoint, + host=cache_endpoint, port=6379, socket_connect_timeout=5, socket_timeout=5, max_connections=1000, ) -persistence_layer = RedisCachePersistenceLayer(client=client) +persistence_layer = CachePersistenceLayer(client=client) @dataclass diff --git a/examples/idempotency/src/getting_started_with_idempotency_valkey_client.py b/examples/idempotency/src/getting_started_with_idempotency_valkey_client.py new file mode 100644 index 00000000000..11a0c710bac --- /dev/null +++ b/examples/idempotency/src/getting_started_with_idempotency_valkey_client.py @@ -0,0 +1,53 @@ +import os +from dataclasses import dataclass, field +from uuid import uuid4 + +from glide import GlideClient, GlideClientConfiguration, NodeAddress + +from aws_lambda_powertools.utilities.idempotency import ( + idempotent, +) +from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +cache_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost") +client_config = GlideClientConfiguration( + addresses=[ + NodeAddress( + host="localhost", + port=6379, + ), + ], +) +client = GlideClient.create(config=client_config) + +persistence_layer = CachePersistenceLayer(client=client) # type: ignore[arg-type] + + +@dataclass +class Payment: + user_id: str + product_id: str + payment_id: str = field(default_factory=lambda: f"{uuid4()}") + + +class PaymentError(Exception): ... + + +@idempotent(persistence_store=persistence_layer) +def lambda_handler(event: dict, context: LambdaContext): + try: + payment: Payment = create_subscription_payment(event) + return { + "payment_id": payment.payment_id, + "message": "success", + "statusCode": 200, + } + except Exception as exc: + raise PaymentError(f"Error creating payment {str(exc)}") + + +def create_subscription_payment(event: dict) -> Payment: + return Payment(**event) diff --git a/examples/idempotency/src/using_redis_client_with_aws_secrets.py b/examples/idempotency/src/using_redis_client_with_aws_secrets.py index ee9e6d78c45..7974abaa595 100644 --- a/examples/idempotency/src/using_redis_client_with_aws_secrets.py +++ b/examples/idempotency/src/using_redis_client_with_aws_secrets.py @@ -2,27 +2,33 @@ from typing import Any -from redis import Redis +from glide import BackoffStrategy, GlideClient, GlideClientConfiguration, NodeAddress, ServerCredentials from aws_lambda_powertools.utilities import parameters from aws_lambda_powertools.utilities.idempotency import IdempotencyConfig, idempotent -from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, +from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) -redis_values: dict[str, Any] = parameters.get_secret("redis_info", transform="json") # (1)! - -redis_client = Redis( - host=redis_values.get("REDIS_HOST", "localhost"), - port=redis_values.get("REDIS_PORT", 6379), - password=redis_values.get("REDIS_PASSWORD"), - decode_responses=True, - socket_timeout=10.0, - ssl=True, - retry_on_timeout=True, +cache_values: dict[str, Any] = parameters.get_secret("cache_info", transform="json") # (1)! + +client_config = GlideClientConfiguration( + addresses=[ + NodeAddress( + host=cache_values.get("REDIS_HOST", "localhost"), + port=cache_values.get("REDIS_PORT", 6379), + ), + ], + credentials=ServerCredentials( + password=cache_values.get("REDIS_PASSWORD", ""), + ), + request_timeout=10, + use_tls=True, + reconnect_strategy=BackoffStrategy(num_of_retries=10, factor=2, exponent_base=1), ) +valkey_client = GlideClient.create(config=client_config) -persistence_layer = RedisCachePersistenceLayer(client=redis_client) +persistence_layer = CachePersistenceLayer(client=valkey_client) # type: ignore[arg-type] config = IdempotencyConfig( expires_after_seconds=2 * 60, # 2 minutes ) diff --git a/examples/idempotency/src/using_redis_client_with_local_certs.py b/examples/idempotency/src/using_redis_client_with_local_certs.py index 2b6a5892c5b..f7be54c386e 100644 --- a/examples/idempotency/src/using_redis_client_with_local_certs.py +++ b/examples/idempotency/src/using_redis_client_with_local_certs.py @@ -7,8 +7,8 @@ from aws_lambda_powertools.shared.functions import abs_lambda_path from aws_lambda_powertools.utilities import parameters from aws_lambda_powertools.utilities.idempotency import IdempotencyConfig, idempotent -from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( - RedisCachePersistenceLayer, +from aws_lambda_powertools.utilities.idempotency.persistence.cache import ( + CachePersistenceLayer, ) redis_values: dict[str, Any] = parameters.get_secret("redis_info", transform="json") # (1)! @@ -27,7 +27,7 @@ ssl_ca_certs=f"{abs_lambda_path()}/certs/redis_ca.pem", # (4)! ) -persistence_layer = RedisCachePersistenceLayer(client=redis_client) +persistence_layer = CachePersistenceLayer(client=redis_client) config = IdempotencyConfig( expires_after_seconds=2 * 60, # 2 minutes ) diff --git a/examples/idempotency/templates/cfn_redis_serverless.yaml b/examples/idempotency/templates/cfn_redis_serverless.yaml index 8ce9d67f3cb..8def8774909 100644 --- a/examples/idempotency/templates/cfn_redis_serverless.yaml +++ b/examples/idempotency/templates/cfn_redis_serverless.yaml @@ -2,30 +2,30 @@ AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Resources: - RedisServerlessIdempotency: + CacheServerlessIdempotency: Type: AWS::ElastiCache::ServerlessCache Properties: Engine: redis ServerlessCacheName: redis-cache SecurityGroupIds: # (1)! - - security-{your_sg_id} + - sg-07d998809154f9d88 SubnetIds: - subnet-{your_subnet_id_1} - subnet-{your_subnet_id_2} - HelloWorldFunction: + IdempotencyFunction: Type: AWS::Serverless::Function Properties: - Runtime: python3.12 + Runtime: python3.13 Handler: app.py VpcConfig: # (1)! SecurityGroupIds: - - security-{your_sg_id} + - sg-07d998809154f9d88 SubnetIds: - subnet-{your_subnet_id_1} - subnet-{your_subnet_id_2} Environment: Variables: POWERTOOLS_SERVICE_NAME: sample - REDIS_HOST: !GetAtt RedisServerlessIdempotency.Endpoint.Address - REDIS_PORT: !GetAtt RedisServerlessIdempotency.Endpoint.Port + CACHE_HOST: !GetAtt CacheServerlessIdempotency.Endpoint.Address + CACHE_PORT: !GetAtt CacheServerlessIdempotency.Endpoint.Port diff --git a/poetry.lock b/poetry.lock index 4250f5d5bd0..a9dd10e733e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -11,7 +11,7 @@ files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [[package]] name = "anyio" @@ -74,7 +74,7 @@ files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] -markers = {main = "extra == \"redis\" and python_full_version < \"3.11.3\"", dev = "python_full_version < \"3.11.3\""} +markers = {main = "python_version < \"3.11\" and (extra == \"redis\" or extra == \"valkey\") or extra == \"redis\" and python_full_version < \"3.11.3\"", dev = "python_full_version < \"3.11.3\""} [[package]] name = "attrs" @@ -331,7 +331,7 @@ description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"tracer\"" +markers = "extra == \"tracer\" or extra == \"all\"" files = [ {file = "aws_xray_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:cfbe6feea3d26613a2a869d14c9246a844285c97087ad8f296f901633554ad94"}, {file = "aws_xray_sdk-2.14.0.tar.gz", hash = "sha256:aab843c331af9ab9ba5cefb3a303832a19db186140894a523edafc024cc0493c"}, @@ -874,8 +874,8 @@ files = [ jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ - {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, ] [package.extras] @@ -1379,7 +1379,7 @@ files = [ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] -markers = {main = "python_version < \"3.10\" and (extra == \"all\" or extra == \"datamasking\")", dev = "python_version < \"3.10\""} +markers = {main = "python_version == \"3.9\" and (extra == \"all\" or extra == \"datamasking\")", dev = "python_version == \"3.9\""} [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} @@ -1560,10 +1560,10 @@ files = [ [package.dependencies] bytecode = [ + {version = ">=0.13.0", markers = "python_version < \"3.11\""}, {version = ">=0.16.0", markers = "python_version >= \"3.13.0\""}, {version = ">=0.15.1", markers = "python_version ~= \"3.12.0\""}, {version = ">=0.14.0", markers = "python_version ~= \"3.11.0\""}, - {version = ">=0.13.0", markers = "python_version < \"3.11.0\""}, ] envier = ">=0.6.1,<0.7.0" legacy-cgi = {version = ">=2.0.0", markers = "python_version >= \"3.13.0\""} @@ -1695,7 +1695,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -1726,7 +1726,7 @@ description = "Fastest Python implementation of JSON schema" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"validation\"" +markers = "extra == \"validation\" or extra == \"all\"" files = [ {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, @@ -2173,8 +2173,6 @@ groups = ["main"] markers = "extra == \"all\" or extra == \"datamasking\"" files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -2927,7 +2925,7 @@ description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, @@ -3168,7 +3166,7 @@ files = [ {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] annotated-types = ">=0.6.0" @@ -3288,7 +3286,7 @@ files = [ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" @@ -4487,7 +4485,7 @@ files = [ {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] typing-extensions = ">=4.12.0" @@ -4640,6 +4638,55 @@ files = [ {file = "uv-0.6.7.tar.gz", hash = "sha256:aa558764265fb69c89c6b5dc7124265d74fb8265d81a91079912df376ff4a3b2"}, ] +[[package]] +name = "valkey-glide" +version = "1.3.5" +description = "An open source Valkey client library that supports Valkey and Redis open source 6.2, 7.0, 7.2 and 8.0." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"valkey\"" +files = [ + {file = "valkey_glide-1.3.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:5335e1c3799b80d61e88369b9d56ea1f9af7f366dad1e0e50678312c09f9230f"}, + {file = "valkey_glide-1.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90a7752309213244df0a9c97484f29dc1133628ba07efbf5581d3709a66d98ba"}, + {file = "valkey_glide-1.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf01955e0035a5fb0c07e08a1090129b883f3c7820d85151d8549ad4ddc62064"}, + {file = "valkey_glide-1.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf9014a26a94a15b765b9f9d76252aedb2cf811ca9cbaad822e7ccec7e1269d"}, + {file = "valkey_glide-1.3.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:df2c5be1782a6cac667983af87734dd9fb89f089820e51beb9a2c593d546ecfe"}, + {file = "valkey_glide-1.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cd7a2066c5785b599798ff59ac6c8cdbc3a25262ca789853e7ecb0c75ff20d9"}, + {file = "valkey_glide-1.3.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d0c019f1605329dabb5f33925b3d9c600feff568b413cdab7304331588455e7"}, + {file = "valkey_glide-1.3.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b55c2cbed655ae249e05f7ae9d048d30a69a0017ec57dfcc0282aaacdfe894f6"}, + {file = "valkey_glide-1.3.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:6046204c9c527d6f07fd8af1babec5661cfc66fa01bec8672f87e283847c9fd8"}, + {file = "valkey_glide-1.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e6c28da4423d0800dfd5ff44f5432ecd1652ac6fa29d91e50eade4c2fe11d09"}, + {file = "valkey_glide-1.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d321c8e27e894fb0eab96f2d1d8c37b4f31ea1a55d67ae087f9127357bd3291"}, + {file = "valkey_glide-1.3.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd69d89cd6c486e2a0c559787e64007f17522e7728d41d063371584c850524e7"}, + {file = "valkey_glide-1.3.5-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:6eaca572835fad1af1e2b480346540f541443ff841c50eaaadf33dfeb5a6fe1d"}, + {file = "valkey_glide-1.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58d2115f10eb7ae385be82bf470613978bc60f9b85c48f887061834774d3efa3"}, + {file = "valkey_glide-1.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44e1bff6cf0fefc099a3b49dd7da6924a96324333f05409c80b792c9137427e8"}, + {file = "valkey_glide-1.3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c2f5aea2ca9aefefc00045b53ce9153ca3a03a6584cc8c2c075c97ac863a0f"}, + {file = "valkey_glide-1.3.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:f9aaf1bac93c0ee24f995fd4d90bd17685909be686f6a92eb20e0b7be1de7155"}, + {file = "valkey_glide-1.3.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5913caf32ebea4682910e07ea1cc6382d1fe594c764cbd4b7112095f8bfe5c4"}, + {file = "valkey_glide-1.3.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1a5e92c2d5b6c804edaaef48493f75d1cef94bb4309b2f7f7ee308b70f836a"}, + {file = "valkey_glide-1.3.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca3618dd6ba1a27e1a479b95fa5f70f4e50383431c8a1ebd01673a2444446c42"}, + {file = "valkey_glide-1.3.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:8aac8a48c34ab8c1885150ad25e4f4b296ae1ba4e405914c6b64ac6034ae6df7"}, + {file = "valkey_glide-1.3.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2d6f41163e19ef3277bf4861a754514e440b93930c0a5b63fe20f563543b4ce"}, + {file = "valkey_glide-1.3.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b613271b2a9ee60c534b2f8bc9eeeffef94600417f10d45ee0400b9f5ebfebd"}, + {file = "valkey_glide-1.3.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703adff16c1ac6ffd1ae9da492502a412179786b779e45da4f0f5e5665de0fb9"}, + {file = "valkey_glide-1.3.5-pp311-pypy311_pp73-macosx_10_7_x86_64.whl", hash = "sha256:56b4abb66b43974e3fac1a070d9bb24dea6fc2ed24b412093920bfd502af2a8a"}, + {file = "valkey_glide-1.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:02ade5c2c1a4b8841011fb220ec126ee884569701934dd2be59d342bfea6cd6d"}, + {file = "valkey_glide-1.3.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c540d627da49f1be714deb0774f0114293d425f90ce58faa52895c7036e67a9a"}, + {file = "valkey_glide-1.3.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a40c2e6d0fba5656f1e3c42251e983ab89e9362455697bf5dd663b9513207f"}, + {file = "valkey_glide-1.3.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:71ae003951b00b2d2000050b989eb2c585c89b5b884b4652e0cc7831d09f9373"}, + {file = "valkey_glide-1.3.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:183ae871bf30de26ca2e4f46b4823e32cd3051656c4c3330921c7c48171bd979"}, + {file = "valkey_glide-1.3.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd894bc91ea6081f30acf4d609b3e6a00e120d32db3de70997c77a1feb3872d"}, + {file = "valkey_glide-1.3.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6ee8e7b787ec6362256bce0ebb004fb93d3be24314c7b3f15d2e6552fca79fc"}, + {file = "valkey_glide-1.3.5.tar.gz", hash = "sha256:4751e6975f24a4f2e788102578ecf7bc4f4ff2f997efc400f547b983b9ec18ac"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_version < \"3.11\""} +protobuf = ">=3.20" +typing-extensions = {version = ">=4.8.0", markers = "python_version < \"3.11\""} + [[package]] name = "verspec" version = "0.1.0" @@ -4882,8 +4929,9 @@ parser = ["pydantic"] redis = ["redis"] tracer = ["aws-xray-sdk"] validation = ["fastjsonschema"] +valkey = ["valkey-glide"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0.0" -content-hash = "bd02477cd0b318c39e1c2c267f05938c289df7275ded9b0e7bb15974bbbde705" +content-hash = "5abdf9443f3ab159a6570ab5d26df53360f6a4784ee46ed05572487f4da6f383" diff --git a/pyproject.toml b/pyproject.toml index a4a27b40ac0..405ea513a71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ pydantic = { version = "^2.4.0", optional = true } pydantic-settings = {version = "^2.6.1", optional = true} boto3 = { version = "^1.34.32", optional = true } redis = { version = ">=4.4,<7.0", optional = true } +valkey-glide = { version = ">=1.3.5,<2.0", optional = true } aws-encryption-sdk = { version = ">=3.1.1,<5.0.0", optional = true } jsonpath-ng = { version = "^1.6.0", optional = true } datadog-lambda = { version = "^6.106.0", optional = true } @@ -61,6 +62,7 @@ parser = ["pydantic"] validation = ["fastjsonschema"] tracer = ["aws-xray-sdk"] redis = ["redis"] +valkey = ["valkey-glide"] all = [ "pydantic", "pydantic-settings", From 1f4935afee0dc0773aedcb502461cd2f497ece57 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 19 May 2025 20:42:41 +0100 Subject: [PATCH 3/3] More changes --- docs/utilities/idempotency.md | 22 +++++++++---------- ...=> using_cache_client_with_aws_secrets.py} | 6 ++--- .../using_redis_client_with_local_certs.py | 14 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) rename examples/idempotency/src/{using_redis_client_with_aws_secrets.py => using_cache_client_with_aws_secrets.py} (86%) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 5ab38ae5ea2..7786813b9e4 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -500,16 +500,16 @@ We recommend using AWS Secrets Manager to store and rotate certificates safely, For advanced configurations, we recommend using an existing Valkey client for optimal compatibility like SSL certificates and timeout. === "Advanced configuration using AWS Secrets" - ```python title="using_redis_client_with_aws_secrets.py" hl_lines="9-11 13 15 25" - --8<-- "examples/idempotency/src/using_redis_client_with_aws_secrets.py" + ```python title="using_cache_client_with_aws_secrets.py" hl_lines="5 9-11 13 15 18 19 23" + --8<-- "examples/idempotency/src/using_cache_client_with_aws_secrets.py" ``` 1. JSON stored: ```json { - "REDIS_ENDPOINT": "127.0.0.1", - "REDIS_PORT": "6379", - "REDIS_PASSWORD": "redis-secret" + "CACHE_HOST": "127.0.0.1", + "CACHE_PORT": "6379", + "CACHE_PASSWORD": "cache-secret" } ``` @@ -521,14 +521,14 @@ For advanced configurations, we recommend using an existing Valkey client for op 1. JSON stored: ```json { - "REDIS_ENDPOINT": "127.0.0.1", - "REDIS_PORT": "6379", - "REDIS_PASSWORD": "redis-secret" + "CACHE_HOST": "127.0.0.1", + "CACHE_PORT": "6379", + "CACHE_PASSWORD": "cache-secret" } ``` - 2. redis_user.crt file stored in the "certs" directory of your Lambda function - 3. redis_user_private.key file stored in the "certs" directory of your Lambda function - 4. redis_ca.pem file stored in the "certs" directory of your Lambda function + 2. cache_user.crt file stored in the "certs" directory of your Lambda function + 3. cache_user_private.key file stored in the "certs" directory of your Lambda function + 4. cache_ca.pem file stored in the "certs" directory of your Lambda function ##### Cache attributes diff --git a/examples/idempotency/src/using_redis_client_with_aws_secrets.py b/examples/idempotency/src/using_cache_client_with_aws_secrets.py similarity index 86% rename from examples/idempotency/src/using_redis_client_with_aws_secrets.py rename to examples/idempotency/src/using_cache_client_with_aws_secrets.py index 7974abaa595..84eb039168e 100644 --- a/examples/idempotency/src/using_redis_client_with_aws_secrets.py +++ b/examples/idempotency/src/using_cache_client_with_aws_secrets.py @@ -15,12 +15,12 @@ client_config = GlideClientConfiguration( addresses=[ NodeAddress( - host=cache_values.get("REDIS_HOST", "localhost"), - port=cache_values.get("REDIS_PORT", 6379), + host=cache_values.get("CACHE_HOST", "localhost"), + port=cache_values.get("CACHE_PORT", 6379), ), ], credentials=ServerCredentials( - password=cache_values.get("REDIS_PASSWORD", ""), + password=cache_values.get("CACHE_PASSWORD", ""), ), request_timeout=10, use_tls=True, diff --git a/examples/idempotency/src/using_redis_client_with_local_certs.py b/examples/idempotency/src/using_redis_client_with_local_certs.py index f7be54c386e..844f5b37e7d 100644 --- a/examples/idempotency/src/using_redis_client_with_local_certs.py +++ b/examples/idempotency/src/using_redis_client_with_local_certs.py @@ -11,20 +11,20 @@ CachePersistenceLayer, ) -redis_values: dict[str, Any] = parameters.get_secret("redis_info", transform="json") # (1)! +cache_values: dict[str, Any] = parameters.get_secret("cache_info", transform="json") # (1)! redis_client = Redis( - host=redis_values.get("REDIS_HOST", "localhost"), - port=redis_values.get("REDIS_PORT", 6379), - password=redis_values.get("REDIS_PASSWORD"), + host=cache_values.get("REDIS_HOST", "localhost"), + port=cache_values.get("REDIS_PORT", 6379), + password=cache_values.get("REDIS_PASSWORD"), decode_responses=True, socket_timeout=10.0, ssl=True, retry_on_timeout=True, - ssl_certfile=f"{abs_lambda_path()}/certs/redis_user.crt", # (2)! - ssl_keyfile=f"{abs_lambda_path()}/certs/redis_user_private.key", # (3)! - ssl_ca_certs=f"{abs_lambda_path()}/certs/redis_ca.pem", # (4)! + ssl_certfile=f"{abs_lambda_path()}/certs/cache_user.crt", # (2)! + ssl_keyfile=f"{abs_lambda_path()}/certs/cache_user_private.key", # (3)! + ssl_ca_certs=f"{abs_lambda_path()}/certs/cache_ca.pem", # (4)! ) persistence_layer = CachePersistenceLayer(client=redis_client)