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,475 @@
1
+ """
2
+ OptimizedGraphSerializer - High-performance serializer with intelligent caching
3
+
4
+ This is the main serializer that combines:
5
+ - RestMeta.GRAPHS configuration
6
+ - Multi-level caching with TTL support
7
+ - Query optimization (select_related/prefetch_related)
8
+ - ujson for optimal JSON performance
9
+ - Thread-safe operations
10
+
11
+ Drop-in replacement for GraphSerializer with significant performance improvements.
12
+ """
13
+
14
+ import time
15
+ import datetime
16
+ import math
17
+ from decimal import Decimal
18
+ from typing import Any, Dict, List, Optional, Union
19
+
20
+ # Performance debugging
21
+ ENABLE_PERF_DEBUG = False
22
+
23
+ # Use ujson for optimal performance
24
+ try:
25
+ import ujson as json
26
+ except ImportError:
27
+ import json
28
+
29
+ from django.db.models import ForeignKey, OneToOneField, ManyToOneRel, ManyToManyField
30
+ from django.db.models import QuerySet, Model
31
+ from django.core.exceptions import FieldDoesNotExist
32
+ from django.http import HttpResponse
33
+
34
+ # Use logit with graceful fallback
35
+ try:
36
+ from mojo.helpers import logit
37
+ logger = logit.get_logger("optimized_serializer", "optimized_serializer.log")
38
+ except Exception:
39
+ import logging
40
+ logger = logging.getLogger("optimized_serializer")
41
+
42
+ from .cache import get_cache_backend, get_cache_key, get_model_cache_ttl
43
+
44
+
45
+ class OptimizedGraphSerializer:
46
+ """
47
+ Ultra-fast serializer with intelligent caching and query optimization.
48
+
49
+ Drop-in replacement for GraphSerializer with:
50
+ - Multi-level caching based on RestMeta.cache_ttl configuration
51
+ - Query optimization with select_related/prefetch_related
52
+ - ujson for optimal JSON performance
53
+ - Thread-safe operations
54
+ - Memory-efficient QuerySet handling
55
+ """
56
+
57
+ def __init__(self, instance, graph="default", many=False, request=None, bypass_cache=False, simple_mode=False, **kwargs):
58
+ """
59
+ Initialize optimized serializer.
60
+
61
+ :param instance: Model instance or QuerySet
62
+ :param graph: Graph configuration name from RestMeta.GRAPHS
63
+ :param many: Force many=True for list serialization
64
+ :param request: Django request object (for compatibility)
65
+ :param bypass_cache: Skip all caching operations for performance testing
66
+ :param simple_mode: Behave exactly like simple serializer (no cache overhead at all)
67
+ :param kwargs: Additional options (cache, etc.)
68
+ """
69
+ self.graph = graph
70
+ self.request = request
71
+ self.instance = instance
72
+ self.many = many
73
+ self.qset = None
74
+ self.bypass_cache = bypass_cache
75
+
76
+ # Simplified cache handling - only initialize if actually needed
77
+ self.bypass_cache = bypass_cache
78
+ self.simple_mode = simple_mode or bypass_cache # simple_mode implies bypass_cache
79
+ self._cache_backend = None
80
+ self._request_cache = None
81
+
82
+ # Handle QuerySet detection - same as simple serializer
83
+ if isinstance(instance, QuerySet):
84
+ self.many = True
85
+ self.qset = instance
86
+ self.instance = list(instance) # Convert QuerySet to list for iteration
87
+ elif many and not isinstance(instance, (list, tuple)):
88
+ # Convert single instance to list for many=True case
89
+ self.instance = [instance]
90
+
91
+ # Performance tracking (only if debug enabled)
92
+ if ENABLE_PERF_DEBUG:
93
+ self._start_time = time.perf_counter()
94
+ self._debug_times = {}
95
+ self._debug_enabled = True
96
+ else:
97
+ self._debug_enabled = False
98
+
99
+ def serialize(self):
100
+ """
101
+ Main serialization method with caching and optimization.
102
+ """
103
+ if self.many:
104
+ return [self._serialize_instance_cached(obj) for obj in self.instance]
105
+ return self._serialize_instance_cached(self.instance)
106
+
107
+
108
+
109
+ def _serialize_instance_cached(self, obj):
110
+ """
111
+ Serialize single instance with intelligent caching.
112
+ """
113
+ # Simple mode: behave exactly like simple serializer
114
+ if self.simple_mode:
115
+ return self._serialize_instance_direct(obj)
116
+
117
+ # Skip all caching if bypass_cache is enabled
118
+ if self.bypass_cache:
119
+ return self._serialize_instance_direct(obj)
120
+
121
+ # Always do request-scoped caching for performance
122
+ cache_key = get_cache_key(obj, self.graph)
123
+ if not cache_key:
124
+ return self._serialize_instance_direct(obj)
125
+
126
+ # Initialize request cache only when needed
127
+ if self._request_cache is None:
128
+ self._request_cache = {}
129
+
130
+ # Check request-scoped cache first (works for all models)
131
+ if cache_key in self._request_cache:
132
+ return self._request_cache[cache_key]
133
+
134
+ # Check if persistent caching is configured
135
+ cache_ttl = get_model_cache_ttl(obj, self.graph)
136
+ cached_result = None
137
+
138
+ if cache_ttl > 0:
139
+ # Try persistent cache only if TTL > 0
140
+ if self._cache_backend is None:
141
+ self._cache_backend = get_cache_backend()
142
+
143
+ cached_result = self._cache_backend.get(cache_key)
144
+ if cached_result is not None:
145
+ # Store in request cache too for faster subsequent access
146
+ self._request_cache[cache_key] = cached_result
147
+ return cached_result
148
+
149
+ # Serialize the result
150
+ result = self._serialize_instance_direct(obj)
151
+
152
+ # Always store in request cache (provides performance gain even when TTL=0)
153
+ self._request_cache[cache_key] = result
154
+
155
+ # Store in persistent cache only if TTL > 0
156
+ if cache_ttl > 0 and self._cache_backend is not None:
157
+ self._cache_backend.set(cache_key, result, cache_ttl)
158
+
159
+ return result
160
+
161
+ def _serialize_instance_direct(self, obj):
162
+ """
163
+ Direct serialization using RestMeta.GRAPHS configuration.
164
+ """
165
+ if not hasattr(obj, "RestMeta") or not hasattr(obj.RestMeta, "GRAPHS"):
166
+ if self._debug_enabled:
167
+ logger.warning(f"RestMeta.GRAPHS not found for {obj.__class__.__name__}")
168
+ return self._fallback_serialization(obj)
169
+
170
+ graph_config = obj.RestMeta.GRAPHS.get(self.graph)
171
+ if graph_config is None:
172
+ if self.graph != "default":
173
+ graph_config = obj.RestMeta.GRAPHS.get("default")
174
+
175
+ if graph_config is None:
176
+ if self._debug_enabled:
177
+ logger.warning(f"No graph '{self.graph}' found for {obj.__class__.__name__}")
178
+ return self._fallback_serialization(obj)
179
+
180
+ # Serialize based on graph configuration
181
+ data = {}
182
+
183
+ # Basic model fields - if no fields specified, use all model fields
184
+ fields = graph_config.get("fields", [])
185
+ if not fields:
186
+ fields = [field.name for field in obj._meta.fields]
187
+
188
+ # Apply exclude filter to remove sensitive fields
189
+ exclude_fields = graph_config.get("exclude", [])
190
+ if exclude_fields:
191
+ fields = [field for field in fields if field not in exclude_fields]
192
+
193
+ for field_name in fields:
194
+ try:
195
+ field_value = getattr(obj, field_name)
196
+ field = self._get_model_field(obj, field_name)
197
+
198
+ # Handle callable attributes
199
+ if callable(field_value):
200
+ try:
201
+ field_value = field_value()
202
+ except Exception as e:
203
+ if self._debug_enabled:
204
+ logger.warning(f"Error calling {field_name}: {e}")
205
+ continue
206
+
207
+ # Serialize the value
208
+ data[field_name] = self._serialize_value_fast(field_value, field)
209
+
210
+ except AttributeError:
211
+ if self._debug_enabled:
212
+ logger.debug(f"Field '{field_name}' not found on {obj.__class__.__name__}")
213
+ continue
214
+
215
+ # Extra fields (methods, properties)
216
+ extra_fields = graph_config.get("extra", [])
217
+ for field_spec in extra_fields:
218
+ if isinstance(field_spec, (tuple, list)):
219
+ method_name, alias = field_spec
220
+ else:
221
+ method_name, alias = field_spec, field_spec
222
+
223
+ try:
224
+ if hasattr(obj, method_name):
225
+ attr = getattr(obj, method_name)
226
+ # For extra fields, we trust the method/property to return a
227
+ # JSON-serializable value, just like the simple serializer does.
228
+ data[alias] = attr() if callable(attr) else attr
229
+ else:
230
+ if self._debug_enabled:
231
+ logger.debug(f"Extra field '{method_name}' not found on {obj.__class__.__name__}")
232
+ except Exception as e:
233
+ if self._debug_enabled:
234
+ logger.warning(f"Error processing extra field '{method_name}': {e}")
235
+ data[alias] = None
236
+
237
+ # Related object graphs
238
+ related_graphs = graph_config.get("graphs", {})
239
+ for field_name, sub_graph in related_graphs.items():
240
+ try:
241
+ related_obj = getattr(obj, field_name, None)
242
+ if related_obj is None:
243
+ data[field_name] = None
244
+ continue
245
+
246
+ field = self._get_model_field(obj, field_name)
247
+
248
+ if isinstance(field, (ForeignKey, OneToOneField)):
249
+ # Single related object - share request cache for performance
250
+ related_serializer = OptimizedGraphSerializer(related_obj, graph=sub_graph, bypass_cache=self.bypass_cache)
251
+ # Share the request cache to avoid re-serializing same objects
252
+ related_serializer._request_cache = self._request_cache
253
+ data[field_name] = related_serializer._serialize_instance_cached(related_obj)
254
+
255
+ elif isinstance(field, (ManyToManyField, ManyToOneRel)) or hasattr(related_obj, 'all'):
256
+ # Many-to-many or reverse relationship - share request cache
257
+ if hasattr(related_obj, 'all'):
258
+ related_qset = related_obj.all()
259
+ related_serializer = OptimizedGraphSerializer(related_qset, graph=sub_graph, many=True, bypass_cache=self.bypass_cache)
260
+ # Share the request cache to avoid re-serializing same objects
261
+ related_serializer._request_cache = self._request_cache
262
+ data[field_name] = related_serializer.serialize()
263
+ else:
264
+ data[field_name] = []
265
+ else:
266
+ data[field_name] = str(related_obj)
267
+
268
+ except Exception as e:
269
+ if self._debug_enabled:
270
+ logger.error(f"Error processing related field '{field_name}': {e}")
271
+ data[field_name] = None
272
+
273
+ return data
274
+
275
+ def _apply_query_optimizations(self, queryset):
276
+ """
277
+ Apply select_related and prefetch_related optimizations based on graph.
278
+ """
279
+ if not hasattr(queryset.model, 'RestMeta') or not hasattr(queryset.model.RestMeta, 'GRAPHS'):
280
+ return queryset
281
+
282
+ graph_config = queryset.model.RestMeta.GRAPHS.get(self.graph, {})
283
+ if not graph_config:
284
+ return queryset
285
+
286
+ # Analyze fields for optimization
287
+ select_related_fields = []
288
+ prefetch_related_fields = []
289
+
290
+ # Check main fields
291
+ for field_name in graph_config.get("fields", []):
292
+ try:
293
+ field = queryset.model._meta.get_field(field_name)
294
+ if isinstance(field, (ForeignKey, OneToOneField)):
295
+ select_related_fields.append(field_name)
296
+ except FieldDoesNotExist:
297
+ continue
298
+
299
+ # Check related graphs
300
+ for field_name in graph_config.get("graphs", {}).keys():
301
+ try:
302
+ field = queryset.model._meta.get_field(field_name)
303
+ if isinstance(field, (ForeignKey, OneToOneField)):
304
+ select_related_fields.append(field_name)
305
+ elif isinstance(field, (ManyToManyField, ManyToOneRel)):
306
+ prefetch_related_fields.append(field_name)
307
+ except FieldDoesNotExist:
308
+ continue
309
+
310
+ # Apply optimizations
311
+ optimized_queryset = queryset
312
+
313
+ if select_related_fields:
314
+ optimized_queryset = optimized_queryset.select_related(*select_related_fields)
315
+
316
+ if prefetch_related_fields:
317
+ optimized_queryset = optimized_queryset.prefetch_related(*prefetch_related_fields)
318
+
319
+ return optimized_queryset
320
+
321
+ def _fallback_serialization(self, obj):
322
+ """
323
+ Fallback serialization when no RestMeta.GRAPHS available.
324
+ """
325
+ if hasattr(obj, '_meta'):
326
+ fields = [field.name for field in obj._meta.fields]
327
+ data = {}
328
+ for field_name in fields:
329
+ try:
330
+ field_value = getattr(obj, field_name)
331
+ field = self._get_model_field(obj, field_name)
332
+ if callable(field_value):
333
+ field_value = field_value()
334
+ data[field_name] = self._serialize_value_fast(field_value, field)
335
+ except:
336
+ continue
337
+ return data
338
+ return str(obj)
339
+
340
+ def _serialize_value_fast(self, value, field=None):
341
+ """
342
+ Fast value serialization optimized for common types.
343
+ """
344
+ if value is None:
345
+ return None
346
+
347
+ # Handle datetime objects (common case first for speed)
348
+ if isinstance(value, datetime.datetime):
349
+ return int(value.timestamp())
350
+ elif isinstance(value, datetime.date):
351
+ return value.isoformat()
352
+
353
+ # Handle foreign key relationships
354
+ if field and isinstance(field, (ForeignKey, OneToOneField)) and hasattr(value, 'pk'):
355
+ return value.pk
356
+
357
+ # Handle model instances
358
+ elif hasattr(value, 'pk') and hasattr(value, '_meta'):
359
+ return value.pk
360
+
361
+ # Handle basic types (most common)
362
+ elif isinstance(value, (str, int, float, bool)):
363
+ return value
364
+
365
+ # Handle numeric types
366
+ elif isinstance(value, Decimal):
367
+ return 0.0 if value.is_nan() else float(value)
368
+ elif isinstance(value, float) and math.isnan(value):
369
+ return 0.0
370
+
371
+ # Handle collections
372
+ elif isinstance(value, (list, tuple)):
373
+ return [self._serialize_value_fast(v) for v in value]
374
+ elif isinstance(value, dict):
375
+ return {k: self._serialize_value_fast(v) for k, v in value.items()}
376
+
377
+ # Default to string conversion
378
+ else:
379
+ return str(value)
380
+
381
+ def _get_model_field(self, obj, field_name):
382
+ """Get Django model field object."""
383
+ try:
384
+ return obj._meta.get_field(field_name)
385
+ except FieldDoesNotExist:
386
+ return None
387
+
388
+ def to_json(self, **kwargs):
389
+ """
390
+ Convert serialized data to JSON string using ujson.
391
+ """
392
+ data = self.serialize()
393
+
394
+ # Build response structure
395
+ if self.many:
396
+ response_data = {
397
+ 'data': data,
398
+ 'status': True,
399
+ 'size': len(data),
400
+ 'graph': self.graph
401
+ }
402
+ else:
403
+ response_data = {
404
+ 'data': data,
405
+ 'status': True,
406
+ 'graph': self.graph
407
+ }
408
+
409
+ # Add any additional kwargs
410
+ response_data.update(kwargs)
411
+
412
+ # Use ujson for optimal performance
413
+ try:
414
+ return json.dumps(response_data)
415
+ except Exception as e:
416
+ logger.error(f"JSON serialization error: {e}")
417
+ # Fallback to standard json with custom encoder
418
+ import json as std_json
419
+ return std_json.dumps(response_data, default=str)
420
+
421
+ def to_response(self, request=None, **kwargs):
422
+ """
423
+ Create HttpResponse with JSON content.
424
+ """
425
+ request = request or self.request
426
+ json_data = self.to_json(**kwargs)
427
+
428
+ response = HttpResponse(json_data, content_type='application/json')
429
+
430
+ # Add performance timing if available (only in debug mode)
431
+ if self._debug_enabled and hasattr(self, '_start_time'):
432
+ elapsed = int((time.perf_counter() - self._start_time) * 1000)
433
+ response['X-Serializer-Time'] = f"{elapsed}ms"
434
+ response['X-Serializer-Type'] = 'optimized'
435
+
436
+ return response
437
+
438
+ def get_performance_info(self):
439
+ """
440
+ Get performance information about this serialization.
441
+ """
442
+ if hasattr(self, '_start_time'):
443
+ elapsed = time.perf_counter() - self._start_time
444
+ cache_backend = self._get_cache_backend() if not self.bypass_cache else None
445
+ perf_info = {
446
+ 'serializer': 'optimized',
447
+ 'graph': self.graph,
448
+ 'many': self.many,
449
+ 'elapsed_seconds': elapsed,
450
+ 'cache_bypassed': self.bypass_cache,
451
+ 'cache_backend': cache_backend.stats().get('backend', 'unknown') if cache_backend else 'bypassed'
452
+ }
453
+
454
+ # Add debug timings if available
455
+ if hasattr(self, '_debug_times') and self._debug_times:
456
+ perf_info['debug_times'] = self._debug_times.copy()
457
+
458
+ # Calculate percentages
459
+ total_time = self._debug_times.get('total_serialize', elapsed)
460
+ if total_time > 0:
461
+ for key, value in self._debug_times.items():
462
+ if key.endswith('_time') and isinstance(value, (int, float)):
463
+ perf_info[f'{key}_percentage'] = (value / total_time) * 100
464
+
465
+ # Log performance issues
466
+ if self._debug_enabled and elapsed > 0.05:
467
+ logger.warning(f"Performance analysis for {self.graph}: {self._debug_times}")
468
+
469
+ return perf_info
470
+ return {}
471
+
472
+
473
+
474
+ # Backwards compatibility alias
475
+ GraphSerializer = OptimizedGraphSerializer