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,454 @@
1
+ """
2
+ Cache Utilities for Django-MOJO Serializers
3
+
4
+ Provides utility functions for cache key generation, TTL extraction from RestMeta.GRAPHS,
5
+ statistics collection, and cache management operations.
6
+
7
+ Key Features:
8
+ - Generate consistent cache keys for model instances
9
+ - Extract cache_ttl from RestMeta.GRAPHS configuration (defaults to 0)
10
+ - Collect statistics from all cache backends
11
+ - Provide cache management operations
12
+ - Thread-safe utility functions
13
+ """
14
+
15
+ import time
16
+ from typing import Any, Optional, Dict, Union
17
+ from django.db.models import Model, QuerySet
18
+
19
+ # Use logit with graceful fallback
20
+ try:
21
+ from mojo.helpers import logit
22
+ logger = logit.get_logger("cache_utils", "cache_utils.log")
23
+ except Exception:
24
+ import logging
25
+ logger = logging.getLogger("cache_utils")
26
+
27
+
28
+ def get_cache_key(instance: Model, graph: str = "default") -> Optional[str]:
29
+ """
30
+ Generate consistent cache key for model instance and graph.
31
+
32
+ Format: "ModelName_pk_graph"
33
+ Example: "Event_123_default", "User_456_list"
34
+
35
+ :param instance: Django model instance
36
+ :param graph: Graph configuration name
37
+ :return: Cache key string or None if instance has no PK
38
+ """
39
+ # Fast check - pk should always exist on Django models
40
+ pk = instance.pk
41
+ if pk is None:
42
+ return None
43
+
44
+ # Use string concatenation for better performance than f-strings
45
+ return instance.__class__.__name__ + "_" + str(pk) + "_" + graph
46
+
47
+
48
+ def get_model_cache_ttl(instance: Model, graph: str = "default") -> int:
49
+ """
50
+ Extract cache TTL from model's RestMeta.GRAPHS configuration.
51
+
52
+ Returns 0 (no caching) by default for safety.
53
+
54
+ :param instance: Django model instance
55
+ :param graph: Graph configuration name
56
+ :return: TTL in seconds (0 = no caching)
57
+ """
58
+ # Fast path: check for RestMeta existence
59
+ rest_meta = getattr(instance, 'RestMeta', None)
60
+ if rest_meta is None:
61
+ return 0
62
+
63
+ # Fast path: check for GRAPHS
64
+ graphs = getattr(rest_meta, 'GRAPHS', None)
65
+ if graphs is None:
66
+ return 0
67
+
68
+ # Get specific graph configuration
69
+ graph_config = graphs.get(graph)
70
+ if graph_config is None:
71
+ # Try default graph as fallback
72
+ graph_config = graphs.get('default')
73
+ if graph_config is None:
74
+ return 0
75
+
76
+ # Extract cache_ttl, default to 0 for safety
77
+ ttl = graph_config.get('cache_ttl', 0)
78
+
79
+ # Ensure TTL is a valid integer
80
+ if not isinstance(ttl, int) or ttl < 0:
81
+ return 0
82
+
83
+ return ttl
84
+
85
+
86
+ def get_cache_stats() -> Dict[str, Any]:
87
+ """
88
+ Get comprehensive cache statistics from all backends.
89
+
90
+ :return: Dictionary with cache statistics
91
+ """
92
+ try:
93
+ from .backends import get_cache_backend
94
+
95
+ backend = get_cache_backend()
96
+ stats = backend.stats()
97
+
98
+ # Add timestamp for monitoring
99
+ stats['timestamp'] = time.time()
100
+ stats['timestamp_human'] = time.strftime('%Y-%m-%d %H:%M:%S')
101
+
102
+ return stats
103
+
104
+ except Exception as e:
105
+ logger.error(f"Error getting cache statistics: {e}")
106
+ return {
107
+ 'error': str(e),
108
+ 'timestamp': time.time()
109
+ }
110
+
111
+
112
+ def clear_all_caches() -> bool:
113
+ """
114
+ Clear all cached items from the cache backend.
115
+
116
+ :return: True if successful
117
+ """
118
+ try:
119
+ from .backends import get_cache_backend, reset_cache_backend
120
+
121
+ backend = get_cache_backend()
122
+ result = backend.clear()
123
+
124
+ # Reset the backend to clear any in-memory state
125
+ reset_cache_backend()
126
+
127
+ logger.info("All serializer caches cleared")
128
+ return result
129
+
130
+ except Exception as e:
131
+ logger.error(f"Error clearing caches: {e}")
132
+ return False
133
+
134
+
135
+ def warm_cache(instances: Union[Model, QuerySet, list], graph: str = "default") -> Dict[str, Any]:
136
+ """
137
+ Pre-warm the cache with serialized instances.
138
+
139
+ Useful for warming cache after deployments or during low-traffic periods.
140
+
141
+ :param instances: Model instance, QuerySet, or list of instances
142
+ :param graph: Graph configuration to use
143
+ :return: Dictionary with warming statistics
144
+ """
145
+ from ..serializer import OptimizedGraphSerializer # Avoid circular import
146
+
147
+ start_time = time.time()
148
+ warmed_count = 0
149
+ error_count = 0
150
+
151
+ # Handle different input types
152
+ if isinstance(instances, Model):
153
+ instances = [instances]
154
+ elif isinstance(instances, QuerySet):
155
+ instances = list(instances)
156
+
157
+ try:
158
+ # Serialize each instance to warm the cache
159
+ for instance in instances:
160
+ try:
161
+ serializer = OptimizedGraphSerializer(instance, graph=graph)
162
+ serializer.serialize() # This will cache the result
163
+ warmed_count += 1
164
+ except Exception as e:
165
+ logger.warning(f"Error warming cache for {instance}: {e}")
166
+ error_count += 1
167
+
168
+ duration = time.time() - start_time
169
+
170
+ stats = {
171
+ 'warmed_count': warmed_count,
172
+ 'error_count': error_count,
173
+ 'duration': duration,
174
+ 'objects_per_second': warmed_count / duration if duration > 0 else 0,
175
+ 'graph': graph
176
+ }
177
+
178
+ logger.info(f"Cache warming completed: {warmed_count} objects in {duration:.2f}s")
179
+ return stats
180
+
181
+ except Exception as e:
182
+ logger.error(f"Cache warming failed: {e}")
183
+ return {
184
+ 'error': str(e),
185
+ 'warmed_count': warmed_count,
186
+ 'error_count': error_count
187
+ }
188
+
189
+
190
+ def invalidate_model_cache(model_class, instance_pk: Any = None, graph: str = None):
191
+ """
192
+ Invalidate cache entries for a specific model.
193
+
194
+ :param model_class: Django model class
195
+ :param instance_pk: Specific instance PK to invalidate (None = all instances)
196
+ :param graph: Specific graph to invalidate (None = all graphs)
197
+ """
198
+ try:
199
+ from .backends import get_cache_backend
200
+
201
+ backend = get_cache_backend()
202
+ model_name = model_class.__name__
203
+
204
+ # Get current cache stats to see what we're working with
205
+ stats = backend.stats()
206
+ if stats.get('current_size', 0) == 0:
207
+ return # Nothing to invalidate
208
+
209
+ deleted_count = 0
210
+
211
+ if instance_pk is not None and graph is not None:
212
+ # Invalidate specific instance + graph
213
+ cache_key = f"{model_name}_{instance_pk}_{graph}"
214
+ if backend.delete(cache_key):
215
+ deleted_count += 1
216
+ else:
217
+ # We need to iterate through cache to find matching keys
218
+ # Note: This is not efficient for large caches, but necessary without key scanning
219
+ logger.warning(f"Bulk invalidation not optimized for {model_name}")
220
+ # For now, just clear all caches
221
+ # TODO: Implement more efficient bulk invalidation when we add Redis
222
+ if backend.clear():
223
+ logger.info(f"Cleared all caches due to {model_name} invalidation")
224
+
225
+ if deleted_count > 0:
226
+ logger.info(f"Invalidated {deleted_count} cache entries for {model_name}")
227
+
228
+ except Exception as e:
229
+ logger.error(f"Error invalidating cache for {model_class.__name__}: {e}")
230
+
231
+
232
+ def get_cache_info() -> Dict[str, Any]:
233
+ """
234
+ Get comprehensive cache information including configuration and status.
235
+
236
+ :return: Dictionary with cache information
237
+ """
238
+ try:
239
+ from django.conf import settings
240
+ from .backends import get_cache_backend
241
+
242
+ # Get backend info
243
+ backend = get_cache_backend()
244
+ stats = backend.stats()
245
+
246
+ # Get configuration
247
+ cache_config = getattr(settings, 'MOJO_SERIALIZER_CACHE', {})
248
+
249
+ return {
250
+ 'backend_type': stats.get('backend', 'unknown'),
251
+ 'configuration': cache_config,
252
+ 'statistics': stats,
253
+ 'memory_usage_estimate': _estimate_memory_usage(stats),
254
+ 'recommendations': _get_cache_recommendations(stats)
255
+ }
256
+
257
+ except Exception as e:
258
+ logger.error(f"Error getting cache info: {e}")
259
+ return {'error': str(e)}
260
+
261
+
262
+ def _estimate_memory_usage(stats: Dict[str, Any]) -> Dict[str, Any]:
263
+ """
264
+ Estimate memory usage based on cache statistics.
265
+
266
+ :param stats: Cache statistics
267
+ :return: Memory usage estimates
268
+ """
269
+ current_size = stats.get('current_size', 0)
270
+
271
+ if current_size == 0:
272
+ return {'estimated_mb': 0, 'estimated_bytes': 0}
273
+
274
+ # Rough estimate: 2KB per cached object (JSON + overhead)
275
+ estimated_bytes = current_size * 2048
276
+ estimated_mb = estimated_bytes / (1024 * 1024)
277
+
278
+ return {
279
+ 'estimated_bytes': estimated_bytes,
280
+ 'estimated_mb': round(estimated_mb, 2),
281
+ 'objects': current_size,
282
+ 'note': 'Rough estimate assuming ~2KB per cached object'
283
+ }
284
+
285
+
286
+ def _get_cache_recommendations(stats: Dict[str, Any]) -> list:
287
+ """
288
+ Generate cache optimization recommendations based on statistics.
289
+
290
+ :param stats: Cache statistics
291
+ :return: List of recommendation strings
292
+ """
293
+ recommendations = []
294
+
295
+ # Hit rate recommendations
296
+ hit_rate = stats.get('hit_rate', 0)
297
+ if hit_rate < 0.3:
298
+ recommendations.append("Low cache hit rate (<30%). Consider increasing cache_ttl values.")
299
+ elif hit_rate > 0.9:
300
+ recommendations.append("Excellent cache hit rate (>90%). Cache is working very well.")
301
+
302
+ # Eviction recommendations
303
+ evictions = stats.get('evictions', 0)
304
+ sets = stats.get('sets', 0)
305
+ if evictions > 0 and sets > 0:
306
+ eviction_rate = evictions / sets
307
+ if eviction_rate > 0.2:
308
+ recommendations.append(f"High eviction rate ({eviction_rate:.1%}). Consider increasing max_size.")
309
+
310
+ # Error recommendations
311
+ errors = stats.get('errors', 0)
312
+ if errors > 0:
313
+ recommendations.append(f"Cache errors detected ({errors}). Check logs for details.")
314
+
315
+ # Expired items
316
+ expired_items = stats.get('expired_items', 0)
317
+ total_requests = stats.get('total_requests', 0)
318
+ if expired_items > 0 and total_requests > 0:
319
+ expired_rate = expired_items / total_requests
320
+ if expired_rate > 0.1:
321
+ recommendations.append(f"High expiration rate ({expired_rate:.1%}). Consider longer cache_ttl values.")
322
+
323
+ if not recommendations:
324
+ recommendations.append("Cache performance looks good!")
325
+
326
+ return recommendations
327
+
328
+
329
+ def is_cacheable(instance: Model, graph: str = "default") -> bool:
330
+ """
331
+ Check if an instance is cacheable based on its RestMeta configuration.
332
+
333
+ :param instance: Django model instance
334
+ :param graph: Graph configuration name
335
+ :return: True if cacheable (TTL > 0)
336
+ """
337
+ return get_model_cache_ttl(instance, graph) > 0
338
+
339
+
340
+ def debug_cache_key(instance: Model, graph: str = "default") -> Dict[str, Any]:
341
+ """
342
+ Debug information for cache key and configuration.
343
+
344
+ Useful for troubleshooting caching issues.
345
+
346
+ :param instance: Django model instance
347
+ :param graph: Graph configuration name
348
+ :return: Debug information dictionary
349
+ """
350
+ try:
351
+ cache_key = get_cache_key(instance, graph)
352
+ ttl = get_model_cache_ttl(instance, graph)
353
+
354
+ # Check if already cached
355
+ from .backends import get_cache_backend
356
+ backend = get_cache_backend()
357
+ cached_value = backend.get(cache_key) if cache_key else None
358
+
359
+ return {
360
+ 'instance': f"{instance.__class__.__name__}(pk={instance.pk})",
361
+ 'graph': graph,
362
+ 'cache_key': cache_key,
363
+ 'ttl': ttl,
364
+ 'is_cacheable': ttl > 0,
365
+ 'is_cached': cached_value is not None,
366
+ 'has_rest_meta': hasattr(instance, 'RestMeta'),
367
+ 'has_graphs': hasattr(instance, 'RestMeta') and hasattr(instance.RestMeta, 'GRAPHS'),
368
+ 'available_graphs': list(instance.RestMeta.GRAPHS.keys()) if hasattr(instance, 'RestMeta') and hasattr(instance.RestMeta, 'GRAPHS') else []
369
+ }
370
+
371
+ except Exception as e:
372
+ return {
373
+ 'error': str(e),
374
+ 'instance': str(instance),
375
+ 'graph': graph
376
+ }
377
+
378
+
379
+ def test_json_performance(test_data: Any = None, iterations: int = 1000) -> Dict[str, Any]:
380
+ """
381
+ Test JSON serialization performance comparing ujson vs standard json.
382
+
383
+ :param test_data: Data to serialize (uses default test data if None)
384
+ :param iterations: Number of iterations to run
385
+ :return: Performance comparison results
386
+ """
387
+ import time
388
+
389
+ # Default test data if none provided
390
+ if test_data is None:
391
+ test_data = {
392
+ 'id': 123,
393
+ 'name': 'Test Object',
394
+ 'created': time.time(),
395
+ 'active': True,
396
+ 'metadata': {
397
+ 'type': 'performance_test',
398
+ 'values': list(range(100)),
399
+ 'nested': {
400
+ 'deep': {
401
+ 'data': 'test_value' * 10
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ results = {
408
+ 'test_data_size': len(str(test_data)),
409
+ 'iterations': iterations,
410
+ 'ujson_available': False,
411
+ 'standard_json_time': 0.0,
412
+ 'ujson_time': 0.0,
413
+ 'speedup_ratio': 1.0,
414
+ 'recommendation': ''
415
+ }
416
+
417
+ # Test standard json
418
+ import json as std_json
419
+ start_time = time.perf_counter()
420
+ for _ in range(iterations):
421
+ json_str = std_json.dumps(test_data, default=str)
422
+ std_json.loads(json_str)
423
+ std_time = time.perf_counter() - start_time
424
+ results['standard_json_time'] = std_time
425
+
426
+ # Test ujson if available
427
+ try:
428
+ import ujson
429
+ results['ujson_available'] = True
430
+
431
+ start_time = time.perf_counter()
432
+ for _ in range(iterations):
433
+ json_str = ujson.dumps(test_data)
434
+ ujson.loads(json_str)
435
+ ujson_time = time.perf_counter() - start_time
436
+ results['ujson_time'] = ujson_time
437
+
438
+ # Calculate speedup
439
+ if ujson_time > 0:
440
+ results['speedup_ratio'] = std_time / ujson_time
441
+ if results['speedup_ratio'] > 2.0:
442
+ results['recommendation'] = f"ujson is {results['speedup_ratio']:.1f}x faster - consider using ujson for better performance"
443
+ else:
444
+ results['recommendation'] = f"ujson is {results['speedup_ratio']:.1f}x faster - moderate improvement"
445
+ else:
446
+ results['recommendation'] = "ujson time measurement failed"
447
+
448
+ except ImportError:
449
+ results['ujson_available'] = False
450
+ results['ujson_time'] = 0
451
+ results['speedup_ratio'] = 1.0
452
+ results['recommendation'] = "ujson not available - install with 'pip install ujson' for better performance"
453
+
454
+ return results
@@ -40,10 +40,18 @@ logger = logit.get_logger("serializer_manager", "serializer_manager.log")
40
40
  _registry_lock = RLock()
41
41
 
42
42
  # Default serializer configurations
43
+ # DEFAULT_SERIALIZERS = {
44
+ # 'simple': 'mojo.serializers.simple.GraphSerializer',
45
+ # 'optimized': 'mojo.serializers.core.serializer.OptimizedGraphSerializer',
46
+ # 'advanced': 'mojo.serializers.advanced.AdvancedGraphSerializer',
47
+ # }
48
+
43
49
  DEFAULT_SERIALIZERS = {
44
- 'simple': 'mojo.serializers.simple.GraphSerializer',
45
- 'optimized': 'mojo.serializers.optimized.OptimizedGraphSerializer',
46
- 'advanced': 'mojo.serializers.advanced.AdvancedGraphSerializer',
50
+ 'optimized': 'mojo.serializers.core.serializer.OptimizedGraphSerializer'
51
+ }
52
+
53
+ FORMAT_SERIALIZERS = {
54
+ 'csv': 'mojo.serializers.formats.csv.CsvFormatter'
47
55
  }
48
56
 
49
57
  # Global serializer registry
@@ -58,7 +66,15 @@ _PERFORMANCE_DATA = {
58
66
 
59
67
 
60
68
  class SerializerRegistry:
61
- """Registry for managing available serializers."""
69
+ """
70
+ Registry for managing available serializers.
71
+
72
+ This registry supports lazy loading of serializers. When a serializer is
73
+ registered using an import path string, it is only imported when the
74
+ `register` method is called. The default serializers are registered
75
+ on the first call to `get_serializer_manager()`, avoiding imports at
76
+ application startup.
77
+ """
62
78
 
63
79
  def __init__(self):
64
80
  self.serializers = {}
@@ -152,6 +168,7 @@ class SerializerManager:
152
168
  self.default_serializer = default_serializer
153
169
  self.performance_tracking = enable_performance_tracking
154
170
  self.registry = _registry
171
+ self.serializer_class = None
155
172
 
156
173
  # Initialize default serializers if not already done
157
174
  self._ensure_default_serializers()
@@ -168,6 +185,12 @@ class SerializerManager:
168
185
  serializer_class_or_path=import_path,
169
186
  is_default=(name == 'optimized') # Set optimized as default
170
187
  )
188
+ for format, import_path in FORMAT_SERIALIZERS.items():
189
+ _registry.register(
190
+ name=format,
191
+ serializer_class_or_path=import_path
192
+ )
193
+
171
194
 
172
195
  def _load_configuration(self):
173
196
  """Load configuration from Django settings."""
@@ -193,6 +216,13 @@ class SerializerManager:
193
216
 
194
217
  def get_serializer(self, instance, graph: str = "default", many: bool = None,
195
218
  serializer_type: str = None, **kwargs):
219
+ if not self.serializer_class:
220
+ self.serializer_class = self.registry.get("optimized")
221
+ return self.serializer_class(instance, graph=graph, many=many, **kwargs)
222
+
223
+
224
+ def get_serializer_old(self, instance, graph: str = "default", many: bool = None,
225
+ serializer_type: str = None, **kwargs):
196
226
  """
197
227
  Get appropriate serializer for the given instance and parameters.
198
228
 
@@ -237,6 +267,12 @@ class SerializerManager:
237
267
  return fallback_class(instance, graph=graph, many=many)
238
268
  raise
239
269
 
270
+ def get_format_serializer(self, format: str):
271
+ SerializerClass = self.registry.get(format)
272
+ if SerializerClass:
273
+ return SerializerClass()
274
+ raise ValueError(f"Serializer for format '{format}' not found")
275
+
240
276
  def serialize(self, instance, graph: str = "default", many: bool = None,
241
277
  serializer_type: str = None, **kwargs):
242
278
  """
@@ -499,3 +535,16 @@ def clear_serializer_caches(serializer_type: str = None):
499
535
  def benchmark_serializers(instance, graph: str = "default", serializer_types: List[str] = None, iterations: int = 10):
500
536
  """Benchmark serializers globally."""
501
537
  return get_serializer_manager().benchmark_serializers(instance, graph, serializer_types, iterations)
538
+
539
+ def list_serializers():
540
+ """List all registered serializers globally."""
541
+ return get_serializer_manager().registry.list_serializers()
542
+
543
+ # Import ujson availability info
544
+ try:
545
+ import ujson
546
+ HAS_UJSON = True
547
+ UJSON_VERSION = getattr(ujson, '__version__', 'unknown')
548
+ except ImportError:
549
+ HAS_UJSON = False
550
+ UJSON_VERSION = None