Coverage for jutil/redis_helpers.py: 78%

50 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 16:40 -0500

1import json 

2import logging 

3from functools import lru_cache 

4from typing import Any, Optional 

5from django.conf import settings 

6from django.core.serializers.json import DjangoJSONEncoder 

7 

8try: 

9 import redis # type: ignore 

10 

11 if int(redis.__version__.split(".", maxsplit=1)[0]) < 3: # type: ignore 11 ↛ 12line 11 didn't jump to line 12, because the condition on line 11 was never true

12 raise Exception("Invalid version") 

13except Exception as err: 

14 raise Exception("Using jutil.redis_helpers requires redis>=3.0.0 installed") from err 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19@lru_cache() 

20def redis_connection_pool(connection_str: str) -> redis.ConnectionPool: 

21 """ 

22 Returns cached Redis connection pool. 

23 :param connection_str: str 

24 :return: redis.ConnectionPool 

25 """ 

26 return redis.ConnectionPool.from_url(connection_str) 

27 

28 

29@lru_cache() 

30def redis_instance(connection_str: str = "") -> redis.Redis: 

31 """ 

32 Returns cached Redis instance using settings.REDIS_URL connection string. 

33 If no connection string is provided, AND settings.REDIS_URL is empty or missing, then 

34 localhost with default Redis settings (no password) is assumed. 

35 :param connection_str: Default connection string is settings.REDIS_URL or 'redis://:@localhost:6379/1?max_connections=50'. 

36 """ 

37 if not connection_str: 37 ↛ 39line 37 didn't jump to line 39, because the condition on line 37 was never false

38 connection_str = settings.REDIS_URL if hasattr(settings, "REDIS_URL") else "redis://:@localhost:6379/1?max_connections=50" # type: ignore 

39 return redis.Redis(connection_pool=redis_connection_pool(connection_str)) 

40 

41 

42def redis_prefix_key(name: str) -> str: 

43 """ 

44 Returns name prefixed with default DB name separated with dot. 

45 For example, if Django default DB name is "my_db" 

46 then "my_key" is returned as "my_db.my_key". 

47 :param name: Key name without prefix 

48 :return: Key name with prefix 

49 """ 

50 return str(settings.DATABASES["default"]["NAME"]) + "." + name 

51 

52 

53def redis_set_bytes(name: str, value: bytes, ex: Optional[int] = None): 

54 """ 

55 Sets value of the key as bytes to Redis. 

56 

57 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

58 then key "my_key" is stored in Redis as "my_db.my_key". 

59 

60 :param name: Key name without DB name prefix 

61 :param value: bytes 

62 :param ex: Optional expire time in seconds 

63 """ 

64 return redis_instance().set(redis_prefix_key(name), value, ex=ex) 

65 

66 

67def redis_get_bytes_or_none(name: str) -> Optional[bytes]: 

68 """ 

69 Returns value of the key as bytes from Redis or None if value missing. 

70 

71 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

72 then key "my_key" is stored in Redis as "my_db.my_key". 

73 

74 :param name: Key name without DB name prefix 

75 :return: bytes or None if value missing 

76 """ 

77 return redis_instance().get(redis_prefix_key(name)) 

78 

79 

80def redis_get_bytes(name: str) -> bytes: 

81 """ 

82 Returns value of the key as bytes from Redis. Raise Exception if value is missing. 

83 

84 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

85 then key "my_key" is stored in Redis as "my_db.my_key". 

86 

87 :param name: Key name without DB name prefix 

88 :return: bytes 

89 """ 

90 buf = redis_get_bytes_or_none(name) 

91 if buf is None: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true

92 raise Exception(f"{redis_prefix_key(name)} not in Redis") 

93 return buf 

94 

95 

96def redis_set_json(name: str, value: Any, ex: Optional[int] = None, cls=DjangoJSONEncoder): 

97 """ 

98 Sets value of the key as JSON to Redis. 

99 

100 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

101 then key "my_key" is stored in Redis as "my_db.my_key". 

102 

103 Stored value is assumed to be JSON and it is serialized before sending. 

104 

105 :param name: Key name without DB name prefix 

106 :param value: Any value that will be serialized as JSON 

107 :param ex: Optional expire time in seconds 

108 :param cls: JSON encoder class (default is DjangoJSONEncoder from Django) 

109 """ 

110 value_bytes = json.dumps(value, cls=cls).encode() 

111 return redis_instance().set(redis_prefix_key(name), value_bytes, ex=ex) 

112 

113 

114def redis_get_json_or_none(name: str) -> Any: 

115 """ 

116 Returns value of the key as JSON from Redis or None if value missing. 

117 

118 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

119 then key "my_key" is stored in Redis as "my_db.my_key". 

120 

121 Stored value is assumed to be JSON and it is deserialized before returning. 

122 

123 :param name: Key name without DB name prefix 

124 :return: bytes deserialized as JSON or None 

125 """ 

126 try: 

127 buf = redis_instance().get(redis_prefix_key(name)) 

128 if buf is not None: 128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true

129 return json.loads(buf) 

130 except Exception as exc: 

131 logger.error("redis_get_json(%s) failed: %s", name, exc) 

132 return None 

133 

134 

135def redis_get_json(name: str) -> Any: 

136 """ 

137 Returns value of the key as JSON from Redis. Raise Exception if value is missing. 

138 

139 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

140 then key "my_key" is stored in Redis as "my_db.my_key". 

141 

142 Stored value is assumed to be JSON and it is deserialized before returning. 

143 

144 :param name: Key name without DB name prefix 

145 :return: bytes deserialized as JSON 

146 """ 

147 buf = redis_instance().get(redis_prefix_key(name)) 

148 if buf is None: 148 ↛ 149line 148 didn't jump to line 149, because the condition on line 148 was never true

149 raise Exception(f"{redis_prefix_key(name)} not in Redis") 

150 return json.loads(buf) 

151 

152 

153def redis_delete(name: str): 

154 """ 

155 Deletes key-value pair from from Redis. 

156 

157 Uses default DB name as a key prefix. For example, if default DB name is "my_db" 

158 then key "my_key" is stored in Redis as "my_db.my_key". 

159 

160 :param name: Key name without DB name prefix 

161 """ 

162 return redis_instance().delete(redis_prefix_key(name))