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,518 @@
1
+ """
2
+ Cache Backend Factory for Django-MOJO Serializers
3
+
4
+ Provides factory functions to create appropriate cache backends based on configuration.
5
+ Handles backend selection, configuration validation, and graceful fallback behavior.
6
+
7
+ Supported Backends:
8
+ - memory: In-memory LRU cache with TTL support
9
+ - redis: Distributed Redis-based caching
10
+ - disabled: No-op cache for development/testing
11
+
12
+ Factory Functions:
13
+ - get_cache_backend(): Get configured cache backend instance
14
+ - create_cache_backend(): Create specific backend type
15
+ - validate_cache_config(): Validate configuration parameters
16
+
17
+ Configuration via Django Settings:
18
+ MOJO_SERIALIZER_CACHE = {
19
+ 'backend': 'memory', # 'memory', 'redis', 'disabled'
20
+ 'memory': {
21
+ 'max_size': 5000,
22
+ 'enable_stats': True
23
+ },
24
+ 'redis': {
25
+ 'host': 'localhost',
26
+ 'port': 6379,
27
+ 'db': 2,
28
+ 'key_prefix': 'mojo:serializer:'
29
+ }
30
+ }
31
+ """
32
+
33
+ import threading
34
+ from typing import Dict, Any, Optional
35
+ from django.conf import settings
36
+
37
+ # Use logit with graceful fallback
38
+ try:
39
+ from mojo.helpers import logit
40
+ logger = logit.get_logger("cache_backends", "cache_backends.log")
41
+ except Exception:
42
+ import logging
43
+ logger = logging.getLogger("cache_backends")
44
+
45
+ from .base import CacheBackend
46
+ from .memory import MemoryCacheBackend
47
+ from .disabled import DisabledCacheBackend
48
+
49
+ # Check ujson availability for optimal JSON performance
50
+ try:
51
+ import ujson
52
+ HAS_UJSON = True
53
+ UJSON_VERSION = getattr(ujson, '__version__', 'unknown')
54
+ except ImportError:
55
+ ujson = None
56
+ HAS_UJSON = False
57
+ UJSON_VERSION = None
58
+
59
+ # Redis backend with graceful import fallback
60
+ try:
61
+ from .redis import RedisCacheBackend
62
+ HAS_REDIS_BACKEND = True
63
+ except ImportError:
64
+ RedisCacheBackend = None
65
+ HAS_REDIS_BACKEND = False
66
+
67
+
68
+
69
+ # Global cache backend instance (lazy initialization)
70
+ _cache_backend_instance = None
71
+ _backend_lock = threading.RLock()
72
+
73
+ # Default configuration
74
+ DEFAULT_CACHE_CONFIG = {
75
+ 'backend': 'memory',
76
+ 'memory': {
77
+ 'max_size': 5000,
78
+ 'enable_stats': True
79
+ },
80
+ 'redis': {
81
+ 'host': 'localhost',
82
+ 'port': 6379,
83
+ 'db': 0,
84
+ 'key_prefix': 'mojo:serializer:',
85
+ 'socket_timeout': 1.0,
86
+ 'socket_connect_timeout': 1.0,
87
+ 'enable_stats': True
88
+ },
89
+ 'disabled': {}
90
+ }
91
+
92
+
93
+ def get_cache_backend() -> CacheBackend:
94
+ """
95
+ Get the configured cache backend instance.
96
+
97
+ Uses lazy initialization and returns the same instance across calls.
98
+ Configuration is read from Django settings with sensible defaults.
99
+ Thread-safe singleton pattern.
100
+
101
+ :return: Configured cache backend instance
102
+ """
103
+ global _cache_backend_instance
104
+
105
+ with _backend_lock:
106
+ if _cache_backend_instance is None:
107
+ _cache_backend_instance = create_cache_backend()
108
+
109
+ return _cache_backend_instance
110
+
111
+
112
+ def create_cache_backend(backend_type: Optional[str] = None, **kwargs) -> CacheBackend:
113
+ """
114
+ Create cache backend instance based on type and configuration.
115
+
116
+ :param backend_type: Backend type ('memory', 'redis', 'disabled') or None for auto-detection
117
+ :param kwargs: Override configuration parameters
118
+ :return: Cache backend instance
119
+ """
120
+ # Get configuration from Django settings
121
+ cache_config = getattr(settings, 'MOJO_SERIALIZER_CACHE', {})
122
+ config = {**DEFAULT_CACHE_CONFIG, **cache_config}
123
+
124
+ # Determine backend type
125
+ if backend_type is None:
126
+ backend_type = config.get('backend', 'memory').lower()
127
+
128
+ # Override with any provided kwargs
129
+ if kwargs:
130
+ config.update(kwargs)
131
+
132
+ # Validate configuration
133
+ validated_config = validate_cache_config(config, backend_type)
134
+
135
+ # Create appropriate backend
136
+ if backend_type == 'disabled':
137
+ logger.info("Creating disabled cache backend")
138
+ return DisabledCacheBackend()
139
+
140
+ elif backend_type == 'redis':
141
+ return _create_redis_backend(validated_config)
142
+
143
+ elif backend_type == 'memory':
144
+ return _create_memory_backend(validated_config)
145
+
146
+ else:
147
+ logger.warning(f"Unknown cache backend type '{backend_type}', falling back to memory")
148
+ return _create_memory_backend(validated_config)
149
+
150
+
151
+ def _create_redis_backend(config: Dict[str, Any]) -> CacheBackend:
152
+ """
153
+ Create Redis cache backend with configuration validation and fallback.
154
+ """
155
+ if not HAS_REDIS_BACKEND:
156
+ logger.warning(
157
+ "Redis backend requested but redis package not available. "
158
+ "Install with: pip install redis. Falling back to memory cache."
159
+ )
160
+ return _create_memory_backend(config)
161
+
162
+ redis_config = config.get('redis', {})
163
+
164
+ try:
165
+ logger.info(f"Creating Redis cache backend: {redis_config.get('host', 'localhost')}:{redis_config.get('port', 6379)}")
166
+ if HAS_UJSON:
167
+ logger.info(f"Using ujson {UJSON_VERSION} for optimal JSON performance")
168
+ else:
169
+ logger.warning("ujson not available - using standard json (slower performance)")
170
+
171
+ backend = RedisCacheBackend(**redis_config)
172
+
173
+ # Test connection
174
+ if not backend.ping():
175
+ raise ConnectionError("Redis ping failed")
176
+
177
+ logger.info("Redis cache backend successfully initialized")
178
+ return backend
179
+
180
+ except Exception as e:
181
+ logger.error(f"Failed to create Redis cache backend: {e}")
182
+ logger.info("Falling back to memory cache backend")
183
+ return _create_memory_backend(config)
184
+
185
+
186
+ def _create_memory_backend(config: Dict[str, Any]) -> CacheBackend:
187
+ """
188
+ Create memory cache backend with configuration.
189
+ """
190
+ memory_config = config.get('memory', {})
191
+ max_size = memory_config.get('max_size', 5000)
192
+ enable_stats = memory_config.get('enable_stats', True)
193
+
194
+ logger.info(f"Creating memory cache backend with max_size={max_size}")
195
+ if HAS_UJSON:
196
+ logger.info(f"Using ujson {UJSON_VERSION} for optimal JSON performance")
197
+ else:
198
+ logger.warning("ujson not available - using standard json (slower performance)")
199
+
200
+ return MemoryCacheBackend(max_size=max_size, enable_stats=enable_stats)
201
+
202
+
203
+ def validate_cache_config(config: Dict[str, Any], backend_type: str) -> Dict[str, Any]:
204
+ """
205
+ Validate and normalize cache configuration.
206
+
207
+ :param config: Raw configuration dictionary
208
+ :param backend_type: Backend type to validate for
209
+ :return: Validated configuration with defaults applied
210
+ """
211
+ validated = config.copy()
212
+
213
+ # Validate backend type
214
+ valid_backends = ['memory', 'redis', 'disabled']
215
+ if backend_type not in valid_backends:
216
+ logger.warning(f"Invalid backend type '{backend_type}', using 'memory'")
217
+ validated['backend'] = 'memory'
218
+ backend_type = 'memory'
219
+
220
+ # Backend-specific validation
221
+ if backend_type == 'memory':
222
+ memory_config = validated.setdefault('memory', {})
223
+ memory_config.setdefault('max_size', 5000)
224
+ memory_config.setdefault('enable_stats', True)
225
+
226
+ # Validate max_size
227
+ max_size = memory_config.get('max_size')
228
+ if not isinstance(max_size, int) or max_size <= 0:
229
+ logger.warning(f"Invalid memory max_size '{max_size}', using 5000")
230
+ memory_config['max_size'] = 5000
231
+
232
+ elif backend_type == 'redis':
233
+ redis_config = validated.setdefault('redis', {})
234
+
235
+ # Set defaults
236
+ redis_config.setdefault('host', 'localhost')
237
+ redis_config.setdefault('port', 6379)
238
+ redis_config.setdefault('db', 0)
239
+ redis_config.setdefault('key_prefix', 'mojo:serializer:')
240
+ redis_config.setdefault('socket_timeout', 1.0)
241
+ redis_config.setdefault('socket_connect_timeout', 1.0)
242
+ redis_config.setdefault('enable_stats', True)
243
+
244
+ # Validate port
245
+ port = redis_config.get('port')
246
+ if not isinstance(port, int) or port <= 0 or port > 65535:
247
+ logger.warning(f"Invalid Redis port '{port}', using 6379")
248
+ redis_config['port'] = 6379
249
+
250
+ # Validate db
251
+ db = redis_config.get('db')
252
+ if not isinstance(db, int) or db < 0:
253
+ logger.warning(f"Invalid Redis db '{db}', using 0")
254
+ redis_config['db'] = 0
255
+
256
+ # Validate timeouts
257
+ for timeout_key in ['socket_timeout', 'socket_connect_timeout']:
258
+ timeout = redis_config.get(timeout_key)
259
+ if not isinstance(timeout, (int, float)) or timeout <= 0:
260
+ logger.warning(f"Invalid Redis {timeout_key} '{timeout}', using 1.0")
261
+ redis_config[timeout_key] = 1.0
262
+
263
+ # Ensure key_prefix ends with colon
264
+ key_prefix = redis_config.get('key_prefix', '')
265
+ if key_prefix and not key_prefix.endswith(':'):
266
+ redis_config['key_prefix'] = f"{key_prefix}:"
267
+
268
+ return validated
269
+
270
+
271
+ def reset_cache_backend():
272
+ """
273
+ Reset the global cache backend instance.
274
+
275
+ Forces recreation of the backend on next access. Useful for testing
276
+ or when configuration changes require a fresh backend instance.
277
+ """
278
+ global _cache_backend_instance
279
+
280
+ with _backend_lock:
281
+ if _cache_backend_instance:
282
+ try:
283
+ _cache_backend_instance.clear()
284
+ if hasattr(_cache_backend_instance, 'close'):
285
+ _cache_backend_instance.close()
286
+ except Exception as e:
287
+ logger.warning(f"Error cleaning up old cache backend: {e}")
288
+
289
+ _cache_backend_instance = None
290
+ logger.info("Cache backend reset - will be recreated on next access")
291
+
292
+
293
+ def get_available_backends() -> Dict[str, Dict[str, Any]]:
294
+ """
295
+ Get information about available cache backends.
296
+
297
+ :return: Dictionary with backend availability and capabilities
298
+ """
299
+ backends = {
300
+ 'memory': {
301
+ 'available': True,
302
+ 'description': 'In-memory LRU cache with TTL support',
303
+ 'features': ['LRU eviction', 'TTL support', 'Thread-safe', 'Statistics'],
304
+ 'suitable_for': ['development', 'single-server', 'testing']
305
+ },
306
+ 'disabled': {
307
+ 'available': True,
308
+ 'description': 'No-op cache that disables caching',
309
+ 'features': ['Zero overhead', 'Debug-friendly'],
310
+ 'suitable_for': ['development', 'testing', 'debugging']
311
+ },
312
+ 'redis': {
313
+ 'available': HAS_REDIS_BACKEND,
314
+ 'description': 'Distributed Redis-based caching',
315
+ 'features': ['Distributed', 'Persistent', 'Native TTL', 'High performance'],
316
+ 'suitable_for': ['production', 'multi-server', 'high-availability'],
317
+ 'requirements': ['redis package', 'Redis server'],
318
+ 'ujson_available': HAS_UJSON,
319
+ 'ujson_version': UJSON_VERSION
320
+ }
321
+ }
322
+
323
+ if not HAS_REDIS_BACKEND:
324
+ backends['redis']['error'] = 'Redis package not installed'
325
+
326
+ # Add ujson info to memory backend as well
327
+ backends['memory']['ujson_available'] = HAS_UJSON
328
+ backends['memory']['ujson_version'] = UJSON_VERSION
329
+
330
+ # Add performance note
331
+ if not HAS_UJSON:
332
+ performance_note = 'Install ujson for 2-5x faster JSON serialization: pip install ujson'
333
+ backends['memory']['performance_note'] = performance_note
334
+ backends['redis']['performance_note'] = performance_note
335
+
336
+ return backends
337
+
338
+
339
+ def test_backend_connectivity(backend_type: str, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
340
+ """
341
+ Test connectivity and functionality of a cache backend.
342
+
343
+ :param backend_type: Backend type to test
344
+ :param config: Optional configuration override
345
+ :return: Test results dictionary
346
+ """
347
+ test_results = {
348
+ 'backend': backend_type,
349
+ 'available': False,
350
+ 'connectivity': False,
351
+ 'functionality': False,
352
+ 'performance': {},
353
+ 'errors': []
354
+ }
355
+
356
+ try:
357
+ # Create temporary backend instance
358
+ if config:
359
+ backend = create_cache_backend(backend_type, **config)
360
+ else:
361
+ # Use existing backend if it matches type
362
+ existing_backend = get_cache_backend()
363
+ if existing_backend.stats().get('backend') == backend_type:
364
+ backend = existing_backend
365
+ else:
366
+ backend = create_cache_backend(backend_type)
367
+
368
+ test_results['available'] = True
369
+
370
+ # Test connectivity
371
+ if hasattr(backend, 'ping'):
372
+ test_results['connectivity'] = backend.ping()
373
+ else:
374
+ test_results['connectivity'] = True
375
+
376
+ if test_results['connectivity']:
377
+ # Test basic functionality
378
+ import time
379
+ test_key = f"test_key_{int(time.time())}"
380
+ test_value = {'test': True, 'timestamp': time.time()}
381
+
382
+ # Test set/get cycle
383
+ start_time = time.perf_counter()
384
+ set_success = backend.set(test_key, test_value, ttl=60)
385
+ set_time = time.perf_counter() - start_time
386
+
387
+ if set_success:
388
+ start_time = time.perf_counter()
389
+ retrieved_value = backend.get(test_key)
390
+ get_time = time.perf_counter() - start_time
391
+
392
+ if retrieved_value == test_value:
393
+ test_results['functionality'] = True
394
+ test_results['performance'] = {
395
+ 'set_time_ms': round(set_time * 1000, 2),
396
+ 'get_time_ms': round(get_time * 1000, 2)
397
+ }
398
+ else:
399
+ test_results['errors'].append("Retrieved value doesn't match set value")
400
+
401
+ # Cleanup
402
+ backend.delete(test_key)
403
+ else:
404
+ test_results['errors'].append("Failed to set test value")
405
+ else:
406
+ test_results['errors'].append("Backend connectivity test failed")
407
+
408
+ except Exception as e:
409
+ test_results['errors'].append(f"Backend test failed: {str(e)}")
410
+ logger.error(f"Error testing {backend_type} backend: {e}")
411
+
412
+ return test_results
413
+
414
+
415
+ def get_cache_health() -> Dict[str, Any]:
416
+ """
417
+ Get comprehensive cache system health information.
418
+
419
+ :return: Health status dictionary
420
+ """
421
+ try:
422
+ backend = get_cache_backend()
423
+ stats = backend.stats()
424
+
425
+ # Calculate health score based on various metrics
426
+ health_score = 100
427
+ issues = []
428
+
429
+ # Check hit rate
430
+ hit_rate = stats.get('hit_rate', 0)
431
+ if hit_rate < 0.5:
432
+ health_score -= 20
433
+ issues.append(f"Low cache hit rate: {hit_rate:.1%}")
434
+
435
+ # Check error rate
436
+ errors = stats.get('errors', 0)
437
+ total_ops = stats.get('sets', 0) + stats.get('hits', 0) + stats.get('misses', 0)
438
+ if total_ops > 0:
439
+ error_rate = errors / total_ops
440
+ if error_rate > 0.05: # 5% error rate
441
+ health_score -= 30
442
+ issues.append(f"High error rate: {error_rate:.1%}")
443
+
444
+ # Check connectivity for Redis
445
+ if stats.get('backend') == 'redis':
446
+ if hasattr(backend, 'ping') and not backend.ping():
447
+ health_score -= 50
448
+ issues.append("Redis connectivity issues")
449
+
450
+ # Determine overall health status
451
+ if health_score >= 90:
452
+ status = 'excellent'
453
+ elif health_score >= 70:
454
+ status = 'good'
455
+ elif health_score >= 50:
456
+ status = 'fair'
457
+ else:
458
+ status = 'poor'
459
+
460
+ return {
461
+ 'status': status,
462
+ 'health_score': health_score,
463
+ 'backend_type': stats.get('backend', 'unknown'),
464
+ 'statistics': stats,
465
+ 'issues': issues,
466
+ 'recommendations': _get_health_recommendations(stats, issues)
467
+ }
468
+
469
+ except Exception as e:
470
+ logger.error(f"Error getting cache health: {e}")
471
+ return {
472
+ 'status': 'error',
473
+ 'health_score': 0,
474
+ 'error': str(e)
475
+ }
476
+
477
+
478
+ def _get_health_recommendations(stats: Dict[str, Any], issues: list) -> list:
479
+ """
480
+ Generate health recommendations based on statistics and issues.
481
+ """
482
+ recommendations = []
483
+
484
+ # Hit rate recommendations
485
+ hit_rate = stats.get('hit_rate', 0)
486
+ if hit_rate < 0.3:
487
+ recommendations.append("Consider increasing cache TTL values in RestMeta.GRAPHS")
488
+ elif hit_rate < 0.5:
489
+ recommendations.append("Review caching strategy - some models may benefit from longer TTL")
490
+
491
+ # Memory recommendations for memory backend
492
+ if stats.get('backend') == 'memory':
493
+ utilization = stats.get('utilization', 0)
494
+ if utilization > 0.9:
495
+ recommendations.append("Memory cache is near capacity - consider increasing max_size")
496
+
497
+ evictions = stats.get('evictions', 0)
498
+ if evictions > 100:
499
+ recommendations.append("High eviction count - consider increasing cache size")
500
+
501
+ # Redis-specific recommendations
502
+ elif stats.get('backend') == 'redis':
503
+ if 'Redis connectivity issues' in issues:
504
+ recommendations.append("Check Redis server status and network connectivity")
505
+
506
+ redis_evictions = stats.get('redis_evicted_keys', 0)
507
+ if redis_evictions > 0:
508
+ recommendations.append("Redis is evicting keys - consider increasing Redis maxmemory")
509
+
510
+ # General recommendations
511
+ errors = stats.get('errors', 0)
512
+ if errors > 10:
513
+ recommendations.append("Multiple cache errors detected - check logs for details")
514
+
515
+ if not recommendations:
516
+ recommendations.append("Cache system is performing well!")
517
+
518
+ return recommendations
@@ -0,0 +1,102 @@
1
+ """
2
+ Abstract Base Cache Backend for Django-MOJO Serializers
3
+
4
+ Defines the interface that all cache backends must implement to ensure consistency
5
+ between memory-based, Redis-based, and other caching systems.
6
+
7
+ All cache backends must implement this interface for plug-and-play compatibility.
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Optional, Dict
12
+
13
+
14
+ class CacheBackend(ABC):
15
+ """
16
+ Abstract base class for serializer cache backends.
17
+
18
+ All cache backends must implement this interface to ensure consistency
19
+ between memory-based and distributed caching systems. This allows for
20
+ seamless switching between different cache implementations.
21
+
22
+ Implementation Requirements:
23
+ - Thread-safe operations
24
+ - TTL support (0 = no expiration)
25
+ - JSON-serializable values only
26
+ - Graceful error handling
27
+ - Performance statistics tracking
28
+ """
29
+
30
+ @abstractmethod
31
+ def get(self, key: str) -> Optional[Any]:
32
+ """
33
+ Retrieve item from cache.
34
+
35
+ Must check TTL expiration and return None for expired items.
36
+ Should update access patterns for LRU backends.
37
+
38
+ :param key: Cache key string
39
+ :return: Cached value or None if not found/expired
40
+ """
41
+ pass
42
+
43
+ @abstractmethod
44
+ def set(self, key: str, value: Any, ttl: int = 0) -> bool:
45
+ """
46
+ Store item in cache with optional TTL.
47
+
48
+ Value must be JSON-serializable. TTL of 0 should mean no caching
49
+ (safe default behavior).
50
+
51
+ :param key: Cache key string
52
+ :param value: Value to cache (must be JSON serializable)
53
+ :param ttl: Time-to-live in seconds (0 = no caching/expiration)
54
+ :return: True if successfully cached, False otherwise
55
+ """
56
+ pass
57
+
58
+ @abstractmethod
59
+ def delete(self, key: str) -> bool:
60
+ """
61
+ Remove specific item from cache.
62
+
63
+ :param key: Cache key string
64
+ :return: True if item was found and removed, False if not found
65
+ """
66
+ pass
67
+
68
+ @abstractmethod
69
+ def clear(self) -> bool:
70
+ """
71
+ Clear all items from cache.
72
+
73
+ Should remove all cached items and reset any internal state.
74
+
75
+ :return: True if cache was successfully cleared
76
+ """
77
+ pass
78
+
79
+ @abstractmethod
80
+ def stats(self) -> Dict[str, Any]:
81
+ """
82
+ Get cache performance statistics.
83
+
84
+ Should return a dictionary with standardized keys for monitoring
85
+ and debugging purposes. Recommended keys:
86
+
87
+ - backend: str (backend type name)
88
+ - current_size: int (number of cached items)
89
+ - max_size: int (maximum capacity, 0 if unlimited)
90
+ - hit_rate: float (0.0-1.0, cache hit percentage)
91
+ - total_requests: int (hits + misses)
92
+ - hits: int (cache hits)
93
+ - misses: int (cache misses)
94
+ - sets: int (items added to cache)
95
+ - deletes: int (items removed from cache)
96
+ - evictions: int (items evicted due to capacity)
97
+ - expired_items: int (items that expired)
98
+ - errors: int (operation errors)
99
+
100
+ :return: Dictionary with cache statistics
101
+ """
102
+ pass