# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from __future__ import annotations from typing import Any import redis from flask_caching.backends.rediscache import RedisCache, RedisSentinelCache from redis.sentinel import Sentinel class RedisCacheBackend(RedisCache): MAX_EVENT_COUNT = 100 def __init__( # pylint: disable=too-many-arguments self, host: str, port: int, password: str | None = None, db: int = 0, default_timeout: int = 300, key_prefix: str | None = None, ssl: bool = False, ssl_certfile: str | None = None, ssl_keyfile: str | None = None, ssl_cert_reqs: str = "required", ssl_ca_certs: str | None = None, **kwargs: Any, ) -> None: super().__init__( host=host, port=port, password=password, db=db, default_timeout=default_timeout, key_prefix=key_prefix, **kwargs, ) self._cache = redis.Redis( host=host, port=port, password=password, db=db, ssl=ssl, ssl_certfile=ssl_certfile, ssl_keyfile=ssl_keyfile, ssl_cert_reqs=ssl_cert_reqs, ssl_ca_certs=ssl_ca_certs, **kwargs, ) def set( self, name: str, value: Any, ex: int | None = None, px: int | None = None, nx: bool = False, xx: bool = False, ) -> bool | None: """ Set the value at key ``name``. :param name: Key name :param value: Value to set :param ex: Expire time in seconds :param px: Expire time in milliseconds :param nx: If True, set only if key does not exist :param xx: If True, set only if key already exists :returns: True if set successfully, None if nx/xx condition not met """ return self._cache.set(name, value, ex=ex, px=px, nx=nx, xx=xx) def delete(self, *names: str) -> int: """ Delete one or more keys. :param names: Key names to delete :returns: Number of keys deleted """ return self._cache.delete(*names) def publish(self, channel: str, message: str) -> int: """ Publish a message to a Redis pub/sub channel. :param channel: The channel name to publish to :param message: The message to publish :returns: Number of subscribers that received the message """ return self._cache.publish(channel, message) def pubsub(self) -> redis.client.PubSub: """ Create a pub/sub subscription object. :returns: PubSub object for subscribing to channels """ return self._cache.pubsub() def xadd( self, stream_name: str, event_data: dict[str, Any], event_id: str = "*", maxlen: int | None = None, ) -> str: return self._cache.xadd(stream_name, event_data, event_id, maxlen) def xrange( self, stream_name: str, start: str = "-", end: str = "+", count: int | None = None, ) -> list[Any]: count = count or self.MAX_EVENT_COUNT return self._cache.xrange(stream_name, start, end, count) @classmethod def from_config(cls, config: dict[str, Any]) -> RedisCacheBackend: kwargs = { "host": config.get("CACHE_REDIS_HOST", "localhost"), "port": config.get("CACHE_REDIS_PORT", 6379), "db": config.get("CACHE_REDIS_DB", 0), "password": config.get("CACHE_REDIS_PASSWORD", None), "key_prefix": config.get("CACHE_KEY_PREFIX", None), "default_timeout": config.get("CACHE_DEFAULT_TIMEOUT", 300), "ssl": config.get("CACHE_REDIS_SSL", False), "ssl_certfile": config.get("CACHE_REDIS_SSL_CERTFILE", None), "ssl_keyfile": config.get("CACHE_REDIS_SSL_KEYFILE", None), "ssl_cert_reqs": config.get("CACHE_REDIS_SSL_CERT_REQS", "required"), "ssl_ca_certs": config.get("CACHE_REDIS_SSL_CA_CERTS", None), } # Handle username separately as it's optional for Redis authentication. if configured_username := config.get("CACHE_REDIS_USER"): kwargs["username"] = configured_username return cls(**kwargs) class RedisSentinelCacheBackend(RedisSentinelCache): MAX_EVENT_COUNT = 100 def __init__( # pylint: disable=too-many-arguments self, sentinels: list[tuple[str, int]], master: str, password: str | None = None, sentinel_password: str | None = None, db: int = 0, default_timeout: int = 300, key_prefix: str = "", ssl: bool = False, ssl_certfile: str | None = None, ssl_keyfile: str | None = None, ssl_cert_reqs: str = "required", ssl_ca_certs: str | None = None, **kwargs: Any, ) -> None: # Sentinel dont directly support SSL # Initialize Sentinel without SSL parameters self._sentinel = Sentinel( sentinels, sentinel_kwargs={ "password": sentinel_password, }, **{ k: v for k, v in kwargs.items() if k not in [ "ssl", "ssl_certfile", "ssl_keyfile", "ssl_cert_reqs", "ssl_ca_certs", ] }, ) # Prepare SSL-related arguments for master_for method master_kwargs = { "password": password, "ssl": ssl, "ssl_certfile": ssl_certfile if ssl else None, "ssl_keyfile": ssl_keyfile if ssl else None, "ssl_cert_reqs": ssl_cert_reqs if ssl else None, "ssl_ca_certs": ssl_ca_certs if ssl else None, } # If SSL is False, remove all SSL-related keys # SSL_* are expected only if SSL is True if not ssl: master_kwargs = { k: v for k, v in master_kwargs.items() if not k.startswith("ssl") } # Filter out None values from master_kwargs master_kwargs = {k: v for k, v in master_kwargs.items() if v is not None} # Initialize Redis master connection self._cache = self._sentinel.master_for(master, **master_kwargs) # Call the parent class constructor super().__init__( host=None, port=None, password=password, db=db, default_timeout=default_timeout, key_prefix=key_prefix, **kwargs, ) def set( self, name: str, value: Any, ex: int | None = None, px: int | None = None, nx: bool = False, xx: bool = False, ) -> bool | None: """ Set the value at key ``name``. :param name: Key name :param value: Value to set :param ex: Expire time in seconds :param px: Expire time in milliseconds :param nx: If True, set only if key does not exist :param xx: If True, set only if key already exists :returns: True if set successfully, None if nx/xx condition not met """ return self._cache.set(name, value, ex=ex, px=px, nx=nx, xx=xx) def delete(self, *names: str) -> int: """ Delete one or more keys. :param names: Key names to delete :returns: Number of keys deleted """ return self._cache.delete(*names) def publish(self, channel: str, message: str) -> int: """ Publish a message to a Redis pub/sub channel. :param channel: The channel name to publish to :param message: The message to publish :returns: Number of subscribers that received the message """ return self._cache.publish(channel, message) def pubsub(self) -> redis.client.PubSub: """ Create a pub/sub subscription object. :returns: PubSub object for subscribing to channels """ return self._cache.pubsub() def xadd( self, stream_name: str, event_data: dict[str, Any], event_id: str = "*", maxlen: int | None = None, ) -> str: return self._cache.xadd(stream_name, event_data, event_id, maxlen) def xrange( self, stream_name: str, start: str = "-", end: str = "+", count: int | None = None, ) -> list[Any]: count = count or self.MAX_EVENT_COUNT return self._cache.xrange(stream_name, start, end, count) @classmethod def from_config(cls, config: dict[str, Any]) -> RedisSentinelCacheBackend: kwargs = { "sentinels": config.get("CACHE_REDIS_SENTINELS", [("127.0.0.1", 26379)]), "master": config.get("CACHE_REDIS_SENTINEL_MASTER", "mymaster"), "password": config.get("CACHE_REDIS_PASSWORD", None), "sentinel_password": config.get("CACHE_REDIS_SENTINEL_PASSWORD", None), "key_prefix": config.get("CACHE_KEY_PREFIX", ""), "db": config.get("CACHE_REDIS_DB", 0), "ssl": config.get("CACHE_REDIS_SSL", False), "ssl_certfile": config.get("CACHE_REDIS_SSL_CERTFILE", None), "ssl_keyfile": config.get("CACHE_REDIS_SSL_KEYFILE", None), "ssl_cert_reqs": config.get("CACHE_REDIS_SSL_CERT_REQS", "required"), "ssl_ca_certs": config.get("CACHE_REDIS_SSL_CA_CERTS", None), } return cls(**kwargs)