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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /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
|