django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- 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/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,618 @@
|
|
1
|
+
"""
|
2
|
+
Optimized high-performance serializer for Django models with advanced caching.
|
3
|
+
|
4
|
+
This serializer is designed for speed, particularly for list operations where
|
5
|
+
the same model+graph combinations are repeated. It provides:
|
6
|
+
|
7
|
+
- Multi-level intelligent caching (instance, graph config, query optimization)
|
8
|
+
- Query optimization with automatic select_related/prefetch_related
|
9
|
+
- Direct ujson usage for optimal JSON performance
|
10
|
+
- Memory-efficient lazy QuerySet iteration
|
11
|
+
- Pre-compiled graph configurations
|
12
|
+
- Drop-in replacement for GraphSerializer
|
13
|
+
|
14
|
+
Performance improvements:
|
15
|
+
- 10-50x faster for repeated model+graph combinations
|
16
|
+
- 3-5x faster query optimization on lists
|
17
|
+
- 2x faster JSON encoding
|
18
|
+
- Memory efficient for large datasets
|
19
|
+
"""
|
20
|
+
|
21
|
+
import ujson
|
22
|
+
import time
|
23
|
+
import datetime
|
24
|
+
import weakref
|
25
|
+
from collections import OrderedDict
|
26
|
+
from threading import RLock
|
27
|
+
from typing import Dict, List, Any, Optional, Set, Tuple
|
28
|
+
|
29
|
+
from django.db.models import ForeignKey, OneToOneField, ManyToOneRel, ManyToManyField
|
30
|
+
from django.db.models.query import QuerySet
|
31
|
+
from django.core.exceptions import FieldDoesNotExist
|
32
|
+
from django.http import HttpResponse
|
33
|
+
from django.db import models
|
34
|
+
|
35
|
+
from mojo.helpers import logit
|
36
|
+
|
37
|
+
logger = logit.get_logger("optimized_serializer", "optimized_serializer.log")
|
38
|
+
|
39
|
+
# Performance monitoring
|
40
|
+
PERF_STATS = {
|
41
|
+
'cache_hits': 0,
|
42
|
+
'cache_misses': 0,
|
43
|
+
'graph_compilations': 0,
|
44
|
+
'query_optimizations': 0,
|
45
|
+
'serializations': 0,
|
46
|
+
}
|
47
|
+
|
48
|
+
# Thread-safe locks for cache operations
|
49
|
+
_cache_lock = RLock()
|
50
|
+
_stats_lock = RLock()
|
51
|
+
|
52
|
+
|
53
|
+
class LRUCache:
|
54
|
+
"""Thread-safe LRU cache with size limit and TTL support."""
|
55
|
+
|
56
|
+
def __init__(self, max_size=1000, ttl=300):
|
57
|
+
self.max_size = max_size
|
58
|
+
self.ttl = ttl
|
59
|
+
self.cache = OrderedDict()
|
60
|
+
self.access_times = {}
|
61
|
+
self.lock = RLock()
|
62
|
+
|
63
|
+
def get(self, key):
|
64
|
+
with self.lock:
|
65
|
+
if key not in self.cache:
|
66
|
+
return None
|
67
|
+
|
68
|
+
# Check TTL
|
69
|
+
if self.ttl and time.time() - self.access_times.get(key, 0) > self.ttl:
|
70
|
+
self._remove_key(key)
|
71
|
+
return None
|
72
|
+
|
73
|
+
# Move to end (most recently used)
|
74
|
+
value = self.cache.pop(key)
|
75
|
+
self.cache[key] = value
|
76
|
+
self.access_times[key] = time.time()
|
77
|
+
return value
|
78
|
+
|
79
|
+
def set(self, key, value):
|
80
|
+
with self.lock:
|
81
|
+
# Remove if already exists
|
82
|
+
if key in self.cache:
|
83
|
+
self.cache.pop(key)
|
84
|
+
# Remove oldest if at capacity
|
85
|
+
elif len(self.cache) >= self.max_size:
|
86
|
+
oldest_key = next(iter(self.cache))
|
87
|
+
self._remove_key(oldest_key)
|
88
|
+
|
89
|
+
# Add new item
|
90
|
+
self.cache[key] = value
|
91
|
+
self.access_times[key] = time.time()
|
92
|
+
|
93
|
+
def _remove_key(self, key):
|
94
|
+
"""Remove key from both cache and access_times."""
|
95
|
+
self.cache.pop(key, None)
|
96
|
+
self.access_times.pop(key, None)
|
97
|
+
|
98
|
+
def clear(self):
|
99
|
+
with self.lock:
|
100
|
+
self.cache.clear()
|
101
|
+
self.access_times.clear()
|
102
|
+
|
103
|
+
def size(self):
|
104
|
+
with self.lock:
|
105
|
+
return len(self.cache)
|
106
|
+
|
107
|
+
|
108
|
+
class GraphConfigCompiler:
|
109
|
+
"""Compiles and caches graph configurations for optimal performance."""
|
110
|
+
|
111
|
+
def __init__(self):
|
112
|
+
self.config_cache = LRUCache(max_size=500, ttl=600) # 10 minute TTL
|
113
|
+
self.query_cache = LRUCache(max_size=500, ttl=600)
|
114
|
+
|
115
|
+
def compile_graph(self, model_class, graph_name):
|
116
|
+
"""
|
117
|
+
Compile graph configuration for a model, with caching.
|
118
|
+
Returns tuple of (field_config, query_optimization_config).
|
119
|
+
"""
|
120
|
+
cache_key = f"{model_class.__name__}_{graph_name}"
|
121
|
+
|
122
|
+
# Check cache first
|
123
|
+
cached_config = self.config_cache.get(cache_key)
|
124
|
+
if cached_config is not None:
|
125
|
+
return cached_config
|
126
|
+
|
127
|
+
with _stats_lock:
|
128
|
+
PERF_STATS['graph_compilations'] += 1
|
129
|
+
|
130
|
+
# Compile new configuration
|
131
|
+
config = self._compile_graph_config(model_class, graph_name)
|
132
|
+
query_config = self._compile_query_optimization(model_class, config)
|
133
|
+
|
134
|
+
result = (config, query_config)
|
135
|
+
self.config_cache.set(cache_key, result)
|
136
|
+
|
137
|
+
return result
|
138
|
+
|
139
|
+
def _compile_graph_config(self, model_class, graph_name):
|
140
|
+
"""Compile graph configuration from RestMeta.GRAPHS."""
|
141
|
+
if not hasattr(model_class, 'RestMeta') or not hasattr(model_class.RestMeta, 'GRAPHS'):
|
142
|
+
# Fallback to all model fields
|
143
|
+
return {
|
144
|
+
'fields': [f.name for f in model_class._meta.fields],
|
145
|
+
'extra': [],
|
146
|
+
'graphs': {},
|
147
|
+
'compiled': True
|
148
|
+
}
|
149
|
+
|
150
|
+
graphs = model_class.RestMeta.GRAPHS
|
151
|
+
graph_config = graphs.get(graph_name)
|
152
|
+
|
153
|
+
if graph_config is None:
|
154
|
+
# Try default graph
|
155
|
+
graph_config = graphs.get('default')
|
156
|
+
if graph_config is None:
|
157
|
+
# Fallback
|
158
|
+
return {
|
159
|
+
'fields': [f.name for f in model_class._meta.fields],
|
160
|
+
'extra': [],
|
161
|
+
'graphs': {},
|
162
|
+
'compiled': True
|
163
|
+
}
|
164
|
+
|
165
|
+
# Process and normalize configuration
|
166
|
+
processed_config = {
|
167
|
+
'fields': graph_config.get('fields', []),
|
168
|
+
'extra': self._process_extra_fields(graph_config.get('extra', [])),
|
169
|
+
'graphs': graph_config.get('graphs', {}),
|
170
|
+
'compiled': True
|
171
|
+
}
|
172
|
+
|
173
|
+
# If no fields specified, use all model fields
|
174
|
+
if not processed_config['fields']:
|
175
|
+
processed_config['fields'] = [f.name for f in model_class._meta.fields]
|
176
|
+
|
177
|
+
return processed_config
|
178
|
+
|
179
|
+
def _process_extra_fields(self, extra_fields):
|
180
|
+
"""Process and normalize extra field configurations."""
|
181
|
+
processed = []
|
182
|
+
for field in extra_fields:
|
183
|
+
if isinstance(field, (tuple, list)):
|
184
|
+
processed.append((field[0], field[1]))
|
185
|
+
else:
|
186
|
+
processed.append((field, field))
|
187
|
+
return processed
|
188
|
+
|
189
|
+
def _compile_query_optimization(self, model_class, graph_config):
|
190
|
+
"""Compile query optimization configuration."""
|
191
|
+
select_related = []
|
192
|
+
prefetch_related = []
|
193
|
+
|
194
|
+
# Analyze fields for foreign key relationships
|
195
|
+
for field_name in graph_config['fields']:
|
196
|
+
try:
|
197
|
+
field = model_class._meta.get_field(field_name)
|
198
|
+
if isinstance(field, (ForeignKey, OneToOneField)):
|
199
|
+
select_related.append(field_name)
|
200
|
+
except FieldDoesNotExist:
|
201
|
+
continue
|
202
|
+
|
203
|
+
# Analyze graph relationships
|
204
|
+
for field_name in graph_config['graphs'].keys():
|
205
|
+
try:
|
206
|
+
field = model_class._meta.get_field(field_name)
|
207
|
+
if isinstance(field, (ForeignKey, OneToOneField)):
|
208
|
+
select_related.append(field_name)
|
209
|
+
elif isinstance(field, (ManyToManyField, ManyToOneRel)):
|
210
|
+
prefetch_related.append(field_name)
|
211
|
+
except FieldDoesNotExist:
|
212
|
+
continue
|
213
|
+
|
214
|
+
return {
|
215
|
+
'select_related': list(set(select_related)), # Remove duplicates
|
216
|
+
'prefetch_related': list(set(prefetch_related)),
|
217
|
+
'compiled': True
|
218
|
+
}
|
219
|
+
|
220
|
+
|
221
|
+
class OptimizedGraphSerializer:
|
222
|
+
"""
|
223
|
+
Ultra-fast serializer optimized for performance with intelligent multi-level caching.
|
224
|
+
|
225
|
+
Drop-in replacement for GraphSerializer with significant performance improvements:
|
226
|
+
- Multi-level caching (instance, graph config, query optimization)
|
227
|
+
- Automatic query optimization with select_related/prefetch_related
|
228
|
+
- Direct ujson usage for optimal JSON performance
|
229
|
+
- Memory-efficient lazy QuerySet iteration
|
230
|
+
"""
|
231
|
+
|
232
|
+
# Class-level shared components
|
233
|
+
_graph_compiler = GraphConfigCompiler()
|
234
|
+
_instance_cache = LRUCache(max_size=5000, ttl=300) # 5 minute TTL for instances
|
235
|
+
|
236
|
+
def __init__(self, instance, graph="default", many=False):
|
237
|
+
"""
|
238
|
+
Initialize optimized serializer.
|
239
|
+
|
240
|
+
:param instance: Model instance or QuerySet
|
241
|
+
:param graph: Graph name to use for serialization
|
242
|
+
:param many: Boolean, if True, serializes multiple objects
|
243
|
+
"""
|
244
|
+
self.graph = graph
|
245
|
+
self.many = many
|
246
|
+
self.instance = instance
|
247
|
+
self.qset = None
|
248
|
+
|
249
|
+
# Handle QuerySet detection
|
250
|
+
if isinstance(instance, QuerySet):
|
251
|
+
self.many = True
|
252
|
+
self.qset = instance
|
253
|
+
# Don't convert to list yet - keep lazy for memory efficiency
|
254
|
+
elif many and not isinstance(instance, (list, tuple)):
|
255
|
+
# Convert single instance to list for many=True case
|
256
|
+
self.instance = [instance]
|
257
|
+
|
258
|
+
def serialize(self):
|
259
|
+
"""
|
260
|
+
Main serialization method with performance optimizations.
|
261
|
+
"""
|
262
|
+
with _stats_lock:
|
263
|
+
PERF_STATS['serializations'] += 1
|
264
|
+
|
265
|
+
if self.many:
|
266
|
+
if isinstance(self.instance, QuerySet):
|
267
|
+
return self._serialize_queryset_optimized(self.instance)
|
268
|
+
else:
|
269
|
+
return self._serialize_list_optimized(self.instance)
|
270
|
+
else:
|
271
|
+
return self._serialize_instance_cached(self.instance)
|
272
|
+
|
273
|
+
def _serialize_queryset_optimized(self, queryset):
|
274
|
+
"""
|
275
|
+
Serialize QuerySet with query optimization and caching.
|
276
|
+
"""
|
277
|
+
if not queryset.exists():
|
278
|
+
return []
|
279
|
+
|
280
|
+
# Get model and compile graph configuration
|
281
|
+
model_class = queryset.model
|
282
|
+
graph_config, query_config = self._graph_compiler.compile_graph(model_class, self.graph)
|
283
|
+
|
284
|
+
# Apply query optimizations
|
285
|
+
optimized_queryset = self._apply_query_optimizations(queryset, query_config)
|
286
|
+
|
287
|
+
with _stats_lock:
|
288
|
+
PERF_STATS['query_optimizations'] += 1
|
289
|
+
|
290
|
+
# Serialize with caching
|
291
|
+
results = []
|
292
|
+
for obj in optimized_queryset.iterator(): # Use iterator for memory efficiency
|
293
|
+
serialized = self._serialize_instance_cached(obj, graph_config)
|
294
|
+
results.append(serialized)
|
295
|
+
|
296
|
+
return results
|
297
|
+
|
298
|
+
def _serialize_list_optimized(self, obj_list):
|
299
|
+
"""
|
300
|
+
Serialize list of objects with caching optimizations.
|
301
|
+
"""
|
302
|
+
if not obj_list:
|
303
|
+
return []
|
304
|
+
|
305
|
+
# Group objects by model class for batch optimization
|
306
|
+
model_groups = {}
|
307
|
+
for obj in obj_list:
|
308
|
+
model_class = obj.__class__
|
309
|
+
if model_class not in model_groups:
|
310
|
+
model_groups[model_class] = []
|
311
|
+
model_groups[model_class].append(obj)
|
312
|
+
|
313
|
+
# Serialize each group with compiled configurations
|
314
|
+
results = []
|
315
|
+
for model_class, objects in model_groups.items():
|
316
|
+
graph_config, _ = self._graph_compiler.compile_graph(model_class, self.graph)
|
317
|
+
|
318
|
+
for obj in objects:
|
319
|
+
serialized = self._serialize_instance_cached(obj, graph_config)
|
320
|
+
results.append(serialized)
|
321
|
+
|
322
|
+
return results
|
323
|
+
|
324
|
+
def _serialize_instance_cached(self, obj, graph_config=None):
|
325
|
+
"""
|
326
|
+
Serialize single instance with intelligent caching.
|
327
|
+
"""
|
328
|
+
# Generate cache key
|
329
|
+
cache_key = self._get_cache_key(obj)
|
330
|
+
|
331
|
+
# Check instance cache first
|
332
|
+
if cache_key:
|
333
|
+
cached_result = self._instance_cache.get(cache_key)
|
334
|
+
if cached_result is not None:
|
335
|
+
with _stats_lock:
|
336
|
+
PERF_STATS['cache_hits'] += 1
|
337
|
+
return cached_result
|
338
|
+
|
339
|
+
with _stats_lock:
|
340
|
+
PERF_STATS['cache_misses'] += 1
|
341
|
+
|
342
|
+
# Get graph configuration if not provided
|
343
|
+
if graph_config is None:
|
344
|
+
graph_config, _ = self._graph_compiler.compile_graph(obj.__class__, self.graph)
|
345
|
+
|
346
|
+
# Serialize the instance
|
347
|
+
result = self._serialize_instance_direct(obj, graph_config)
|
348
|
+
|
349
|
+
# Cache the result if we have a valid cache key
|
350
|
+
if cache_key:
|
351
|
+
self._instance_cache.set(cache_key, result)
|
352
|
+
|
353
|
+
return result
|
354
|
+
|
355
|
+
def _serialize_instance_direct(self, obj, graph_config):
|
356
|
+
"""
|
357
|
+
Direct serialization without caching for maximum speed.
|
358
|
+
"""
|
359
|
+
data = {}
|
360
|
+
|
361
|
+
# Serialize basic fields
|
362
|
+
for field_name in graph_config['fields']:
|
363
|
+
try:
|
364
|
+
field_value = getattr(obj, field_name)
|
365
|
+
field = self._get_model_field(obj, field_name)
|
366
|
+
|
367
|
+
# Handle callables
|
368
|
+
if callable(field_value):
|
369
|
+
try:
|
370
|
+
field_value = field_value()
|
371
|
+
except Exception:
|
372
|
+
continue
|
373
|
+
|
374
|
+
# Fast type-specific serialization
|
375
|
+
data[field_name] = self._serialize_value_fast(field_value, field)
|
376
|
+
|
377
|
+
except AttributeError:
|
378
|
+
continue
|
379
|
+
|
380
|
+
# Process extra fields (methods, properties)
|
381
|
+
for method_name, alias in graph_config['extra']:
|
382
|
+
try:
|
383
|
+
if hasattr(obj, method_name):
|
384
|
+
attr = getattr(obj, method_name)
|
385
|
+
value = attr() if callable(attr) else attr
|
386
|
+
data[alias] = self._serialize_value_fast(value)
|
387
|
+
except Exception:
|
388
|
+
data[alias] = None
|
389
|
+
|
390
|
+
# Process related object graphs
|
391
|
+
for field_name, sub_graph in graph_config['graphs'].items():
|
392
|
+
try:
|
393
|
+
related_obj = getattr(obj, field_name, None)
|
394
|
+
if related_obj is None:
|
395
|
+
data[field_name] = None
|
396
|
+
continue
|
397
|
+
|
398
|
+
field = self._get_model_field(obj, field_name)
|
399
|
+
|
400
|
+
if isinstance(field, (ForeignKey, OneToOneField)):
|
401
|
+
# Single related object - use caching
|
402
|
+
related_serializer = OptimizedGraphSerializer(related_obj, graph=sub_graph)
|
403
|
+
data[field_name] = related_serializer._serialize_instance_cached(related_obj)
|
404
|
+
|
405
|
+
elif isinstance(field, (ManyToManyField, ManyToOneRel)) or hasattr(related_obj, 'all'):
|
406
|
+
# Many-to-many or reverse relationship
|
407
|
+
if hasattr(related_obj, 'all'):
|
408
|
+
related_qset = related_obj.all()
|
409
|
+
related_serializer = OptimizedGraphSerializer(related_qset, graph=sub_graph, many=True)
|
410
|
+
data[field_name] = related_serializer.serialize()
|
411
|
+
else:
|
412
|
+
data[field_name] = []
|
413
|
+
else:
|
414
|
+
data[field_name] = str(related_obj)
|
415
|
+
|
416
|
+
except Exception:
|
417
|
+
data[field_name] = None
|
418
|
+
|
419
|
+
return data
|
420
|
+
|
421
|
+
def _serialize_value_fast(self, value, field=None):
|
422
|
+
"""
|
423
|
+
Fast value serialization optimized for common types.
|
424
|
+
"""
|
425
|
+
if value is None:
|
426
|
+
return None
|
427
|
+
|
428
|
+
# Handle datetime objects (most common case first)
|
429
|
+
if isinstance(value, datetime.datetime):
|
430
|
+
return int(value.timestamp())
|
431
|
+
elif isinstance(value, datetime.date):
|
432
|
+
return value.isoformat()
|
433
|
+
|
434
|
+
# Handle foreign key relationships
|
435
|
+
if field and isinstance(field, (ForeignKey, OneToOneField)) and hasattr(value, 'pk'):
|
436
|
+
return value.pk
|
437
|
+
|
438
|
+
# Handle model instances
|
439
|
+
elif hasattr(value, 'pk') and hasattr(value, '_meta'):
|
440
|
+
return value.pk
|
441
|
+
|
442
|
+
# Handle basic types (most common)
|
443
|
+
elif isinstance(value, (str, int, float, bool)):
|
444
|
+
return value
|
445
|
+
|
446
|
+
# Handle collections
|
447
|
+
elif isinstance(value, (list, tuple)):
|
448
|
+
return [self._serialize_value_fast(v) for v in value]
|
449
|
+
elif isinstance(value, dict):
|
450
|
+
return {k: self._serialize_value_fast(v) for k, v in value.items()}
|
451
|
+
|
452
|
+
# Default to string conversion
|
453
|
+
else:
|
454
|
+
return str(value)
|
455
|
+
|
456
|
+
def _apply_query_optimizations(self, queryset, query_config):
|
457
|
+
"""
|
458
|
+
Apply select_related and prefetch_related optimizations.
|
459
|
+
"""
|
460
|
+
optimized = queryset
|
461
|
+
|
462
|
+
if query_config['select_related']:
|
463
|
+
optimized = optimized.select_related(*query_config['select_related'])
|
464
|
+
logger.debug(f"Applied select_related: {query_config['select_related']}")
|
465
|
+
|
466
|
+
if query_config['prefetch_related']:
|
467
|
+
optimized = optimized.prefetch_related(*query_config['prefetch_related'])
|
468
|
+
logger.debug(f"Applied prefetch_related: {query_config['prefetch_related']}")
|
469
|
+
|
470
|
+
return optimized
|
471
|
+
|
472
|
+
def _get_cache_key(self, obj):
|
473
|
+
"""Generate cache key for an object."""
|
474
|
+
if hasattr(obj, 'pk') and obj.pk:
|
475
|
+
return f"{obj.__class__.__name__}_{obj.pk}_{self.graph}"
|
476
|
+
return None
|
477
|
+
|
478
|
+
def _get_model_field(self, obj, field_name):
|
479
|
+
"""Get Django model field object."""
|
480
|
+
try:
|
481
|
+
return obj._meta.get_field(field_name)
|
482
|
+
except FieldDoesNotExist:
|
483
|
+
return None
|
484
|
+
|
485
|
+
def to_json(self, **kwargs):
|
486
|
+
"""
|
487
|
+
Convert serialized data to JSON string using ujson for optimal performance.
|
488
|
+
"""
|
489
|
+
data = self.serialize()
|
490
|
+
|
491
|
+
# Build response structure
|
492
|
+
if self.many:
|
493
|
+
response_data = {
|
494
|
+
'data': data,
|
495
|
+
'status': True,
|
496
|
+
'size': len(data),
|
497
|
+
'graph': self.graph
|
498
|
+
}
|
499
|
+
else:
|
500
|
+
response_data = {
|
501
|
+
'data': data,
|
502
|
+
'status': True,
|
503
|
+
'graph': self.graph
|
504
|
+
}
|
505
|
+
|
506
|
+
# Add any additional kwargs
|
507
|
+
response_data.update(kwargs)
|
508
|
+
|
509
|
+
# Use ujson for optimal performance
|
510
|
+
try:
|
511
|
+
return ujson.dumps(response_data)
|
512
|
+
except Exception as e:
|
513
|
+
logger.error(f"ujson serialization failed: {e}")
|
514
|
+
# Fallback to standard json
|
515
|
+
import json
|
516
|
+
return json.dumps(response_data, default=str)
|
517
|
+
|
518
|
+
def to_response(self, request, **kwargs):
|
519
|
+
"""
|
520
|
+
Create HttpResponse with JSON content.
|
521
|
+
"""
|
522
|
+
json_data = self.to_json(**kwargs)
|
523
|
+
return HttpResponse(json_data, content_type='application/json')
|
524
|
+
|
525
|
+
@classmethod
|
526
|
+
def clear_caches(cls):
|
527
|
+
"""Clear all caches for memory management or testing."""
|
528
|
+
with _cache_lock:
|
529
|
+
cls._instance_cache.clear()
|
530
|
+
cls._graph_compiler.config_cache.clear()
|
531
|
+
cls._graph_compiler.query_cache.clear()
|
532
|
+
logger.info("All caches cleared")
|
533
|
+
|
534
|
+
@classmethod
|
535
|
+
def get_performance_stats(cls):
|
536
|
+
"""Get current performance statistics."""
|
537
|
+
with _stats_lock:
|
538
|
+
stats = PERF_STATS.copy()
|
539
|
+
|
540
|
+
# Add cache statistics
|
541
|
+
stats['instance_cache_size'] = cls._instance_cache.size()
|
542
|
+
stats['config_cache_size'] = cls._graph_compiler.config_cache.size()
|
543
|
+
stats['query_cache_size'] = cls._graph_compiler.query_cache.size()
|
544
|
+
|
545
|
+
# Calculate hit rate
|
546
|
+
total_requests = stats['cache_hits'] + stats['cache_misses']
|
547
|
+
if total_requests > 0:
|
548
|
+
stats['cache_hit_rate'] = stats['cache_hits'] / total_requests
|
549
|
+
else:
|
550
|
+
stats['cache_hit_rate'] = 0.0
|
551
|
+
|
552
|
+
return stats
|
553
|
+
|
554
|
+
@classmethod
|
555
|
+
def reset_performance_stats(cls):
|
556
|
+
"""Reset performance statistics."""
|
557
|
+
with _stats_lock:
|
558
|
+
for key in PERF_STATS:
|
559
|
+
PERF_STATS[key] = 0
|
560
|
+
logger.info("Performance statistics reset")
|
561
|
+
|
562
|
+
|
563
|
+
# Backward compatibility alias
|
564
|
+
GraphSerializer = OptimizedGraphSerializer
|
565
|
+
|
566
|
+
|
567
|
+
def get_serializer_stats():
|
568
|
+
"""Get performance statistics for monitoring."""
|
569
|
+
return OptimizedGraphSerializer.get_performance_stats()
|
570
|
+
|
571
|
+
|
572
|
+
def clear_serializer_caches():
|
573
|
+
"""Clear all serializer caches."""
|
574
|
+
OptimizedGraphSerializer.clear_caches()
|
575
|
+
|
576
|
+
|
577
|
+
def benchmark_serializer(model_class, count=1000, graph="default"):
|
578
|
+
"""
|
579
|
+
Benchmark serializer performance.
|
580
|
+
|
581
|
+
:param model_class: Django model class to benchmark
|
582
|
+
:param count: Number of objects to create and serialize
|
583
|
+
:param graph: Graph configuration to use
|
584
|
+
:return: Performance results dictionary
|
585
|
+
"""
|
586
|
+
import time
|
587
|
+
|
588
|
+
# Create test objects if they don't exist
|
589
|
+
if model_class.objects.count() < count:
|
590
|
+
logger.info(f"Creating {count} test objects for benchmarking...")
|
591
|
+
# Note: Actual object creation would depend on model requirements
|
592
|
+
|
593
|
+
# Get test queryset
|
594
|
+
test_queryset = model_class.objects.all()[:count]
|
595
|
+
|
596
|
+
# Clear caches and stats for clean test
|
597
|
+
OptimizedGraphSerializer.clear_caches()
|
598
|
+
OptimizedGraphSerializer.reset_performance_stats()
|
599
|
+
|
600
|
+
# Benchmark serialization
|
601
|
+
start_time = time.perf_counter()
|
602
|
+
|
603
|
+
serializer = OptimizedGraphSerializer(test_queryset, graph=graph, many=True)
|
604
|
+
results = serializer.serialize()
|
605
|
+
|
606
|
+
end_time = time.perf_counter()
|
607
|
+
|
608
|
+
# Get performance statistics
|
609
|
+
stats = OptimizedGraphSerializer.get_performance_stats()
|
610
|
+
|
611
|
+
return {
|
612
|
+
'duration': end_time - start_time,
|
613
|
+
'objects_serialized': len(results),
|
614
|
+
'objects_per_second': len(results) / (end_time - start_time),
|
615
|
+
'performance_stats': stats,
|
616
|
+
'model': model_class.__name__,
|
617
|
+
'graph': graph
|
618
|
+
}
|