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