django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,533 @@
1
+ """
2
+ Redis Cache Backend for Django-MOJO Serializers
3
+
4
+ Distributed Redis-based cache implementation with TTL support, connection pooling,
5
+ and high availability. Provides shared caching across multiple Django processes
6
+ and servers with automatic expiration and advanced Redis features.
7
+
8
+ Key Features:
9
+ - Distributed caching across multiple processes/servers
10
+ - Native Redis TTL support with automatic expiration
11
+ - JSON serialization for cross-platform compatibility
12
+ - Connection pooling for high performance
13
+ - Configurable key prefixes for multi-tenancy
14
+ - Pipeline operations for batch operations
15
+ - Thread-safe operations
16
+ - Graceful error handling and fallback behavior
17
+ - Comprehensive statistics tracking
18
+
19
+ Usage:
20
+ cache = RedisCacheBackend(
21
+ host='localhost',
22
+ port=6379,
23
+ db=2,
24
+ key_prefix='prod:mojo:serializer:'
25
+ )
26
+
27
+ # Same interface as other backends
28
+ cache.set("Event_123_default", serialized_data, ttl=300)
29
+ data = cache.get("Event_123_default")
30
+
31
+ Configuration:
32
+ MOJO_SERIALIZER_CACHE = {
33
+ 'backend': 'redis',
34
+ 'redis': {
35
+ 'host': 'localhost',
36
+ 'port': 6379,
37
+ 'db': 2,
38
+ 'key_prefix': 'mojo:serializer:',
39
+ 'socket_timeout': 1.0,
40
+ 'connection_pool_kwargs': {
41
+ 'max_connections': 50
42
+ }
43
+ }
44
+ }
45
+ """
46
+
47
+ # Use ujson for optimal performance, fallback to standard json
48
+ try:
49
+ import ujson as json
50
+ except ImportError:
51
+ import json
52
+ import time
53
+ import threading
54
+ from typing import Any, Optional, Dict
55
+ # Use logit with graceful fallback
56
+ try:
57
+ from mojo.helpers import logit
58
+ logger = logit.get_logger("redis_cache", "redis_cache.log")
59
+ except Exception:
60
+ import logging
61
+ logger = logging.getLogger("redis_cache")
62
+
63
+ from .base import CacheBackend
64
+
65
+ # Redis imports with graceful fallback
66
+ try:
67
+ import redis
68
+ from redis.connection import ConnectionPool
69
+ from redis.exceptions import RedisError, ConnectionError, TimeoutError
70
+ HAS_REDIS = True
71
+ except ImportError:
72
+ redis = None
73
+ ConnectionPool = None
74
+ RedisError = Exception
75
+ ConnectionError = Exception
76
+ TimeoutError = Exception
77
+ HAS_REDIS = False
78
+
79
+
80
+ class RedisCacheBackend(CacheBackend):
81
+ """
82
+ Redis-based distributed cache backend with connection pooling and high availability.
83
+
84
+ Provides high-performance distributed caching using Redis with native TTL support,
85
+ connection pooling, JSON serialization, and comprehensive error handling.
86
+
87
+ Features:
88
+ - Distributed caching across multiple processes/servers
89
+ - Native Redis TTL with automatic expiration
90
+ - JSON serialization for cross-platform compatibility
91
+ - Connection pooling for high performance
92
+ - Configurable key prefixes for multi-tenancy
93
+ - Thread-safe operations with connection sharing
94
+ - Graceful error handling and fallback behavior
95
+ - Pipeline operations for batch operations
96
+ - Comprehensive monitoring and statistics
97
+ """
98
+
99
+ def __init__(self, host: str = 'localhost', port: int = 6379, db: int = 0,
100
+ key_prefix: str = 'mojo:serializer:', password: Optional[str] = None,
101
+ socket_timeout: float = 1.0, socket_connect_timeout: float = 1.0,
102
+ connection_pool_kwargs: Optional[Dict] = None, enable_stats: bool = True,
103
+ **kwargs):
104
+ """
105
+ Initialize Redis cache backend with connection pooling.
106
+
107
+ :param host: Redis server hostname
108
+ :param port: Redis server port
109
+ :param db: Redis database number
110
+ :param key_prefix: Prefix for all cache keys
111
+ :param password: Redis authentication password
112
+ :param socket_timeout: Socket timeout in seconds
113
+ :param socket_connect_timeout: Connection timeout in seconds
114
+ :param connection_pool_kwargs: Additional connection pool parameters
115
+ :param enable_stats: Enable statistics tracking
116
+ :param kwargs: Additional Redis connection parameters
117
+ """
118
+ if not HAS_REDIS:
119
+ raise ImportError(
120
+ "Redis backend requires the 'redis' package. "
121
+ "Install with: pip install redis"
122
+ )
123
+
124
+ self.host = host
125
+ self.port = port
126
+ self.db = db
127
+ self.key_prefix = key_prefix
128
+ self.password = password
129
+ self.socket_timeout = socket_timeout
130
+ self.socket_connect_timeout = socket_connect_timeout
131
+ self.enable_stats = enable_stats
132
+
133
+ # Prepare connection pool kwargs
134
+ pool_kwargs = {
135
+ 'host': host,
136
+ 'port': port,
137
+ 'db': db,
138
+ 'password': password,
139
+ 'socket_timeout': socket_timeout,
140
+ 'socket_connect_timeout': socket_connect_timeout,
141
+ 'decode_responses': False, # We handle JSON decoding manually
142
+ 'max_connections': 50, # Default max connections
143
+ }
144
+
145
+ if connection_pool_kwargs:
146
+ pool_kwargs.update(connection_pool_kwargs)
147
+
148
+ # Additional Redis client kwargs
149
+ self.redis_kwargs = kwargs
150
+
151
+ # Create connection pool
152
+ try:
153
+ self._connection_pool = ConnectionPool(**pool_kwargs)
154
+ self._redis_client = redis.Redis(connection_pool=self._connection_pool, **self.redis_kwargs)
155
+
156
+ # Test connection
157
+ self._redis_client.ping()
158
+ logger.info(f"Redis cache backend initialized: {host}:{port}/{db}")
159
+
160
+ except Exception as e:
161
+ logger.error(f"Failed to initialize Redis cache backend: {e}")
162
+ raise
163
+
164
+ # Thread-safe statistics tracking
165
+ self._stats = {
166
+ 'hits': 0,
167
+ 'misses': 0,
168
+ 'sets': 0,
169
+ 'deletes': 0,
170
+ 'evictions': 0, # Redis-managed evictions
171
+ 'expired_items': 0,
172
+ 'errors': 0,
173
+ 'connection_errors': 0,
174
+ 'timeout_errors': 0,
175
+ 'json_errors': 0
176
+ }
177
+ self._stats_lock = threading.RLock()
178
+
179
+ def get(self, key: str) -> Optional[Any]:
180
+ """
181
+ Retrieve item from Redis cache with JSON deserialization.
182
+
183
+ Uses Redis GET command with automatic TTL checking. Returns None
184
+ for cache misses, expired items, or connection errors.
185
+ """
186
+ try:
187
+ redis_key = f"{self.key_prefix}{key}"
188
+ json_data = self._redis_client.get(redis_key)
189
+
190
+ if json_data is None:
191
+ self._increment_stat('misses')
192
+ return None
193
+
194
+ # Deserialize from JSON
195
+ try:
196
+ value = json.loads(json_data.decode('utf-8'))
197
+ self._increment_stat('hits')
198
+ return value
199
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
200
+ logger.warning(f"JSON decode error for key '{key}': {e}")
201
+ self._increment_stat('json_errors')
202
+ # Remove corrupted entry
203
+ self._redis_client.delete(redis_key)
204
+ self._increment_stat('misses')
205
+ return None
206
+
207
+ except (ConnectionError, TimeoutError) as e:
208
+ logger.warning(f"Redis connection error for get '{key}': {e}")
209
+ self._increment_stat('connection_errors')
210
+ self._increment_stat('misses')
211
+ return None
212
+
213
+ except RedisError as e:
214
+ logger.warning(f"Redis error for get '{key}': {e}")
215
+ self._increment_stat('errors')
216
+ self._increment_stat('misses')
217
+ return None
218
+
219
+ def set(self, key: str, value: Any, ttl: int = 0) -> bool:
220
+ """
221
+ Store item in Redis cache with TTL and JSON serialization.
222
+
223
+ Uses Redis SETEX for TTL or SET for no expiration. Handles JSON
224
+ serialization and connection errors gracefully.
225
+ """
226
+ if ttl == 0:
227
+ return True # TTL of 0 means no caching - safe default
228
+
229
+ try:
230
+ redis_key = f"{self.key_prefix}{key}"
231
+
232
+ # Serialize to JSON
233
+ try:
234
+ json_data = json.dumps(value, default=str) # default=str handles dates/decimals
235
+ except (TypeError, ValueError) as e:
236
+ logger.warning(f"JSON encode error for key '{key}': {e}")
237
+ self._increment_stat('json_errors')
238
+ return False
239
+
240
+ # Store in Redis with TTL
241
+ if ttl > 0:
242
+ success = self._redis_client.setex(redis_key, ttl, json_data)
243
+ else:
244
+ success = self._redis_client.set(redis_key, json_data)
245
+
246
+ if success:
247
+ self._increment_stat('sets')
248
+ return True
249
+ else:
250
+ self._increment_stat('errors')
251
+ return False
252
+
253
+ except (ConnectionError, TimeoutError) as e:
254
+ logger.warning(f"Redis connection error for set '{key}': {e}")
255
+ self._increment_stat('connection_errors')
256
+ return False
257
+
258
+ except RedisError as e:
259
+ logger.warning(f"Redis error for set '{key}': {e}")
260
+ self._increment_stat('errors')
261
+ return False
262
+
263
+ def delete(self, key: str) -> bool:
264
+ """
265
+ Remove specific item from Redis cache.
266
+
267
+ Uses Redis DEL command and returns True if the key was actually deleted.
268
+ """
269
+ try:
270
+ redis_key = f"{self.key_prefix}{key}"
271
+ deleted_count = self._redis_client.delete(redis_key)
272
+
273
+ if deleted_count > 0:
274
+ self._increment_stat('deletes')
275
+ return True
276
+ return False
277
+
278
+ except (ConnectionError, TimeoutError) as e:
279
+ logger.warning(f"Redis connection error for delete '{key}': {e}")
280
+ self._increment_stat('connection_errors')
281
+ return False
282
+
283
+ except RedisError as e:
284
+ logger.warning(f"Redis error for delete '{key}': {e}")
285
+ self._increment_stat('errors')
286
+ return False
287
+
288
+ def clear(self) -> bool:
289
+ """
290
+ Clear all cache items with matching key prefix.
291
+
292
+ Uses Redis SCAN for memory-efficient key discovery and DEL for removal.
293
+ This is safer than KEYS * on production Redis instances.
294
+ """
295
+ try:
296
+ pattern = f"{self.key_prefix}*"
297
+ deleted_count = 0
298
+
299
+ # Use SCAN for memory-efficient key iteration
300
+ for key in self._redis_client.scan_iter(match=pattern, count=1000):
301
+ try:
302
+ if self._redis_client.delete(key):
303
+ deleted_count += 1
304
+ except RedisError:
305
+ continue # Skip failed deletes
306
+
307
+ if deleted_count > 0:
308
+ logger.info(f"Cleared {deleted_count} keys from Redis cache")
309
+
310
+ return True
311
+
312
+ except (ConnectionError, TimeoutError) as e:
313
+ logger.error(f"Redis connection error for clear: {e}")
314
+ self._increment_stat('connection_errors')
315
+ return False
316
+
317
+ except RedisError as e:
318
+ logger.error(f"Redis error for clear: {e}")
319
+ self._increment_stat('errors')
320
+ return False
321
+
322
+ def stats(self) -> Dict[str, Any]:
323
+ """
324
+ Get comprehensive Redis cache statistics.
325
+
326
+ Combines local operation statistics with Redis server information
327
+ for complete monitoring and debugging capabilities.
328
+ """
329
+ with self._stats_lock:
330
+ total_requests = self._stats['hits'] + self._stats['misses']
331
+ hit_rate = (self._stats['hits'] / total_requests) if total_requests > 0 else 0.0
332
+
333
+ base_stats = {
334
+ # Backend identification
335
+ 'backend': 'redis',
336
+
337
+ # Connection information
338
+ 'host': self.host,
339
+ 'port': self.port,
340
+ 'db': self.db,
341
+ 'key_prefix': self.key_prefix,
342
+
343
+ # Performance metrics
344
+ 'hit_rate': hit_rate,
345
+ 'total_requests': total_requests,
346
+
347
+ # Operation statistics
348
+ **self._stats.copy()
349
+ }
350
+
351
+ # Add Redis server information if available
352
+ try:
353
+ redis_info = self._redis_client.info()
354
+ base_stats.update({
355
+ 'redis_version': redis_info.get('redis_version', 'unknown'),
356
+ 'redis_memory_used': redis_info.get('used_memory', 0),
357
+ 'redis_memory_used_human': redis_info.get('used_memory_human', '0B'),
358
+ 'redis_connected_clients': redis_info.get('connected_clients', 0),
359
+ 'redis_total_commands_processed': redis_info.get('total_commands_processed', 0),
360
+ 'redis_keyspace_hits': redis_info.get('keyspace_hits', 0),
361
+ 'redis_keyspace_misses': redis_info.get('keyspace_misses', 0),
362
+ 'redis_evicted_keys': redis_info.get('evicted_keys', 0),
363
+ 'redis_expired_keys': redis_info.get('expired_keys', 0),
364
+ 'connection_pool_created_connections': self._connection_pool.created_connections,
365
+ 'connection_pool_available_connections': len(self._connection_pool._available_connections),
366
+ 'connection_pool_in_use_connections': len(self._connection_pool._in_use_connections),
367
+ })
368
+
369
+ # Calculate Redis cache hit rate
370
+ redis_hits = redis_info.get('keyspace_hits', 0)
371
+ redis_misses = redis_info.get('keyspace_misses', 0)
372
+ redis_total = redis_hits + redis_misses
373
+ if redis_total > 0:
374
+ base_stats['redis_hit_rate'] = redis_hits / redis_total
375
+ else:
376
+ base_stats['redis_hit_rate'] = 0.0
377
+
378
+ except Exception as e:
379
+ logger.warning(f"Could not get Redis info: {e}")
380
+ base_stats['redis_info_error'] = str(e)
381
+
382
+ return base_stats
383
+
384
+ def ping(self) -> bool:
385
+ """
386
+ Test Redis connection health.
387
+
388
+ Sends Redis PING command and returns True if Redis responds.
389
+ """
390
+ try:
391
+ return self._redis_client.ping()
392
+ except Exception:
393
+ return False
394
+
395
+ def get_key_count(self) -> int:
396
+ """
397
+ Get approximate count of keys with our prefix.
398
+
399
+ Uses Redis EVAL with Lua script for efficient counting.
400
+ """
401
+ try:
402
+ # Lua script to count keys with prefix
403
+ lua_script = """
404
+ local count = 0
405
+ local keys = redis.call('SCAN', 0, 'MATCH', ARGV[1], 'COUNT', 1000)
406
+ for i = 1, #keys[2] do
407
+ count = count + 1
408
+ end
409
+ return count
410
+ """
411
+
412
+ pattern = f"{self.key_prefix}*"
413
+ count = self._redis_client.eval(lua_script, 0, pattern)
414
+ return count or 0
415
+
416
+ except Exception as e:
417
+ logger.warning(f"Error getting key count: {e}")
418
+ return 0
419
+
420
+ def batch_get(self, keys: list) -> Dict[str, Any]:
421
+ """
422
+ Get multiple keys in a single Redis operation using pipeline.
423
+
424
+ More efficient than individual get operations for multiple keys.
425
+ """
426
+ if not keys:
427
+ return {}
428
+
429
+ try:
430
+ redis_keys = [f"{self.key_prefix}{key}" for key in keys]
431
+
432
+ # Use pipeline for batch operations
433
+ pipe = self._redis_client.pipeline()
434
+ for redis_key in redis_keys:
435
+ pipe.get(redis_key)
436
+
437
+ results = pipe.execute()
438
+
439
+ # Process results
440
+ batch_results = {}
441
+ for i, (original_key, json_data) in enumerate(zip(keys, results)):
442
+ if json_data is not None:
443
+ try:
444
+ value = json.loads(json_data.decode('utf-8'))
445
+ batch_results[original_key] = value
446
+ self._increment_stat('hits')
447
+ except (json.JSONDecodeError, UnicodeDecodeError):
448
+ self._increment_stat('json_errors')
449
+ self._increment_stat('misses')
450
+ else:
451
+ self._increment_stat('misses')
452
+
453
+ return batch_results
454
+
455
+ except Exception as e:
456
+ logger.warning(f"Error in batch_get: {e}")
457
+ self._increment_stat('errors')
458
+ return {}
459
+
460
+ def batch_set(self, items: Dict[str, Any], ttl: int = 0) -> bool:
461
+ """
462
+ Set multiple keys in a single Redis operation using pipeline.
463
+
464
+ More efficient than individual set operations for multiple keys.
465
+ """
466
+ if not items or ttl == 0:
467
+ return True
468
+
469
+ try:
470
+ # Use pipeline for batch operations
471
+ pipe = self._redis_client.pipeline()
472
+
473
+ for key, value in items.items():
474
+ redis_key = f"{self.key_prefix}{key}"
475
+
476
+ try:
477
+ json_data = json.dumps(value, default=str)
478
+ if ttl > 0:
479
+ pipe.setex(redis_key, ttl, json_data)
480
+ else:
481
+ pipe.set(redis_key, json_data)
482
+ except (TypeError, ValueError):
483
+ logger.warning(f"JSON encode error for key '{key}' in batch_set")
484
+ self._increment_stat('json_errors')
485
+ continue
486
+
487
+ results = pipe.execute()
488
+
489
+ # Count successful operations
490
+ success_count = sum(1 for result in results if result)
491
+ self._stats['sets'] += success_count
492
+
493
+ return len(results) == success_count
494
+
495
+ except Exception as e:
496
+ logger.warning(f"Error in batch_set: {e}")
497
+ self._increment_stat('errors')
498
+ return False
499
+
500
+ def close(self):
501
+ """
502
+ Close Redis connection and cleanup resources.
503
+
504
+ Closes connection pool and resets connection state.
505
+ """
506
+ try:
507
+ if self._connection_pool:
508
+ self._connection_pool.disconnect()
509
+ logger.info("Redis cache backend connections closed")
510
+ except Exception as e:
511
+ logger.warning(f"Error closing Redis connections: {e}")
512
+ finally:
513
+ self._connection_pool = None
514
+ self._redis_client = None
515
+
516
+ def _increment_stat(self, stat_name: str):
517
+ """
518
+ Thread-safe statistics increment.
519
+
520
+ :param stat_name: Statistics counter name
521
+ """
522
+ if self.enable_stats:
523
+ with self._stats_lock:
524
+ self._stats[stat_name] += 1
525
+
526
+ def __del__(self):
527
+ """
528
+ Cleanup on object destruction.
529
+ """
530
+ try:
531
+ self.close()
532
+ except Exception:
533
+ pass # Ignore cleanup errors during destruction