django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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 (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.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