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,531 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for serializer administration and benchmarking.
|
3
|
+
|
4
|
+
Provides comprehensive serializer management capabilities including:
|
5
|
+
- Performance benchmarking and comparison
|
6
|
+
- Serializer registration and configuration
|
7
|
+
- Cache management and statistics
|
8
|
+
- Health checks and diagnostics
|
9
|
+
|
10
|
+
Usage:
|
11
|
+
# List all registered serializers
|
12
|
+
python manage.py serializer_admin list
|
13
|
+
|
14
|
+
# Benchmark serializers
|
15
|
+
python manage.py serializer_admin benchmark --model MyModel --count 1000
|
16
|
+
|
17
|
+
# Set default serializer
|
18
|
+
python manage.py serializer_admin set-default optimized
|
19
|
+
|
20
|
+
# Get performance statistics
|
21
|
+
python manage.py serializer_admin stats
|
22
|
+
|
23
|
+
# Clear caches
|
24
|
+
python manage.py serializer_admin clear-cache
|
25
|
+
"""
|
26
|
+
|
27
|
+
import time
|
28
|
+
import json
|
29
|
+
from django.core.management.base import BaseCommand, CommandError
|
30
|
+
from django.apps import apps
|
31
|
+
from django.db import models
|
32
|
+
from django.core.management import color
|
33
|
+
|
34
|
+
from mojo.serializers import (
|
35
|
+
get_serializer_manager,
|
36
|
+
get_performance_stats,
|
37
|
+
clear_serializer_caches,
|
38
|
+
benchmark_serializers,
|
39
|
+
list_serializers,
|
40
|
+
set_default_serializer
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
class Command(BaseCommand):
|
45
|
+
help = 'Manage MOJO serializers: benchmark, configure, and monitor performance'
|
46
|
+
|
47
|
+
def add_arguments(self, parser):
|
48
|
+
"""Add command line arguments."""
|
49
|
+
|
50
|
+
subparsers = parser.add_subparsers(dest='action', help='Available actions')
|
51
|
+
|
52
|
+
# List serializers
|
53
|
+
list_parser = subparsers.add_parser('list', help='List all registered serializers')
|
54
|
+
list_parser.add_argument(
|
55
|
+
'--detail',
|
56
|
+
action='store_true',
|
57
|
+
help='Show detailed information about each serializer'
|
58
|
+
)
|
59
|
+
|
60
|
+
# Benchmark serializers
|
61
|
+
bench_parser = subparsers.add_parser('benchmark', help='Benchmark serializer performance')
|
62
|
+
bench_parser.add_argument(
|
63
|
+
'--model',
|
64
|
+
type=str,
|
65
|
+
required=True,
|
66
|
+
help='Model name to benchmark (format: app.ModelName or ModelName)'
|
67
|
+
)
|
68
|
+
bench_parser.add_argument(
|
69
|
+
'--count',
|
70
|
+
type=int,
|
71
|
+
default=100,
|
72
|
+
help='Number of objects to serialize (default: 100)'
|
73
|
+
)
|
74
|
+
bench_parser.add_argument(
|
75
|
+
'--graph',
|
76
|
+
type=str,
|
77
|
+
default='default',
|
78
|
+
help='Graph configuration to use (default: "default")'
|
79
|
+
)
|
80
|
+
bench_parser.add_argument(
|
81
|
+
'--iterations',
|
82
|
+
type=int,
|
83
|
+
default=5,
|
84
|
+
help='Number of benchmark iterations (default: 5)'
|
85
|
+
)
|
86
|
+
bench_parser.add_argument(
|
87
|
+
'--serializers',
|
88
|
+
nargs='+',
|
89
|
+
help='Specific serializers to benchmark (default: all)'
|
90
|
+
)
|
91
|
+
bench_parser.add_argument(
|
92
|
+
'--output-json',
|
93
|
+
type=str,
|
94
|
+
help='Save results to JSON file'
|
95
|
+
)
|
96
|
+
|
97
|
+
# Set default serializer
|
98
|
+
default_parser = subparsers.add_parser('set-default', help='Set default serializer')
|
99
|
+
default_parser.add_argument(
|
100
|
+
'serializer_name',
|
101
|
+
type=str,
|
102
|
+
help='Name of serializer to set as default'
|
103
|
+
)
|
104
|
+
|
105
|
+
# Performance statistics
|
106
|
+
stats_parser = subparsers.add_parser('stats', help='Show performance statistics')
|
107
|
+
stats_parser.add_argument(
|
108
|
+
'--clear',
|
109
|
+
action='store_true',
|
110
|
+
help='Clear statistics after showing them'
|
111
|
+
)
|
112
|
+
stats_parser.add_argument(
|
113
|
+
'--json',
|
114
|
+
action='store_true',
|
115
|
+
help='Output statistics as JSON'
|
116
|
+
)
|
117
|
+
|
118
|
+
# Cache management
|
119
|
+
cache_parser = subparsers.add_parser('clear-cache', help='Clear serializer caches')
|
120
|
+
cache_parser.add_argument(
|
121
|
+
'--serializer',
|
122
|
+
type=str,
|
123
|
+
help='Clear cache for specific serializer (default: all)'
|
124
|
+
)
|
125
|
+
|
126
|
+
# Health check
|
127
|
+
health_parser = subparsers.add_parser('health', help='Run serializer health checks')
|
128
|
+
health_parser.add_argument(
|
129
|
+
'--model',
|
130
|
+
type=str,
|
131
|
+
help='Test specific model (format: app.ModelName or ModelName)'
|
132
|
+
)
|
133
|
+
|
134
|
+
# Test serializers
|
135
|
+
test_parser = subparsers.add_parser('test', help='Test serializer functionality')
|
136
|
+
test_parser.add_argument(
|
137
|
+
'--model',
|
138
|
+
type=str,
|
139
|
+
required=True,
|
140
|
+
help='Model to test (format: app.ModelName or ModelName)'
|
141
|
+
)
|
142
|
+
test_parser.add_argument(
|
143
|
+
'--graph',
|
144
|
+
type=str,
|
145
|
+
default='default',
|
146
|
+
help='Graph configuration to test'
|
147
|
+
)
|
148
|
+
|
149
|
+
def handle(self, *args, **options):
|
150
|
+
"""Handle command execution."""
|
151
|
+
action = options.get('action')
|
152
|
+
|
153
|
+
if not action:
|
154
|
+
self.print_help()
|
155
|
+
return
|
156
|
+
|
157
|
+
# Route to appropriate handler
|
158
|
+
handler_map = {
|
159
|
+
'list': self.handle_list,
|
160
|
+
'benchmark': self.handle_benchmark,
|
161
|
+
'set-default': self.handle_set_default,
|
162
|
+
'stats': self.handle_stats,
|
163
|
+
'clear-cache': self.handle_clear_cache,
|
164
|
+
'health': self.handle_health,
|
165
|
+
'test': self.handle_test,
|
166
|
+
}
|
167
|
+
|
168
|
+
handler = handler_map.get(action)
|
169
|
+
if handler:
|
170
|
+
try:
|
171
|
+
handler(options)
|
172
|
+
except Exception as e:
|
173
|
+
raise CommandError(f"Error executing {action}: {str(e)}")
|
174
|
+
else:
|
175
|
+
raise CommandError(f"Unknown action: {action}")
|
176
|
+
|
177
|
+
def handle_list(self, options):
|
178
|
+
"""List all registered serializers."""
|
179
|
+
serializers = list_serializers()
|
180
|
+
|
181
|
+
if not serializers:
|
182
|
+
self.stdout.write(
|
183
|
+
self.style.WARNING('No serializers registered')
|
184
|
+
)
|
185
|
+
return
|
186
|
+
|
187
|
+
self.stdout.write(
|
188
|
+
self.style.SUCCESS('Registered Serializers:')
|
189
|
+
)
|
190
|
+
|
191
|
+
for name, info in serializers.items():
|
192
|
+
status = " (DEFAULT)" if info['is_default'] else ""
|
193
|
+
self.stdout.write(f" • {name}{status}")
|
194
|
+
|
195
|
+
if options.get('detail'):
|
196
|
+
self.stdout.write(f" Class: {info['class_name']}")
|
197
|
+
self.stdout.write(f" Description: {info['description']}")
|
198
|
+
|
199
|
+
def handle_benchmark(self, options):
|
200
|
+
"""Benchmark serializer performance."""
|
201
|
+
model_class = self.get_model_class(options['model'])
|
202
|
+
count = options['count']
|
203
|
+
graph = options['graph']
|
204
|
+
iterations = options['iterations']
|
205
|
+
serializer_types = options.get('serializers')
|
206
|
+
|
207
|
+
# Check if model has enough instances
|
208
|
+
total_instances = model_class.objects.count()
|
209
|
+
if total_instances < count:
|
210
|
+
self.stdout.write(
|
211
|
+
self.style.WARNING(
|
212
|
+
f"Model {model_class.__name__} only has {total_instances} instances, "
|
213
|
+
f"but {count} requested. Using available instances."
|
214
|
+
)
|
215
|
+
)
|
216
|
+
count = min(count, total_instances)
|
217
|
+
|
218
|
+
if count == 0:
|
219
|
+
raise CommandError(f"No instances found for model {model_class.__name__}")
|
220
|
+
|
221
|
+
# Get test queryset
|
222
|
+
test_queryset = model_class.objects.all()[:count]
|
223
|
+
|
224
|
+
self.stdout.write(
|
225
|
+
self.style.SUCCESS(
|
226
|
+
f"Benchmarking serializers with {count} {model_class.__name__} instances"
|
227
|
+
)
|
228
|
+
)
|
229
|
+
self.stdout.write(f"Graph: {graph}")
|
230
|
+
self.stdout.write(f"Iterations: {iterations}")
|
231
|
+
|
232
|
+
# Run benchmark
|
233
|
+
try:
|
234
|
+
results = benchmark_serializers(
|
235
|
+
instance=test_queryset,
|
236
|
+
graph=graph,
|
237
|
+
serializer_types=serializer_types,
|
238
|
+
iterations=iterations
|
239
|
+
)
|
240
|
+
|
241
|
+
# Display results
|
242
|
+
self.display_benchmark_results(results)
|
243
|
+
|
244
|
+
# Save to JSON if requested
|
245
|
+
if options.get('output_json'):
|
246
|
+
self.save_benchmark_results(results, options['output_json'])
|
247
|
+
|
248
|
+
except Exception as e:
|
249
|
+
raise CommandError(f"Benchmark failed: {str(e)}")
|
250
|
+
|
251
|
+
def handle_set_default(self, options):
|
252
|
+
"""Set default serializer."""
|
253
|
+
serializer_name = options['serializer_name']
|
254
|
+
|
255
|
+
if set_default_serializer(serializer_name):
|
256
|
+
self.stdout.write(
|
257
|
+
self.style.SUCCESS(
|
258
|
+
f"Default serializer set to: {serializer_name}"
|
259
|
+
)
|
260
|
+
)
|
261
|
+
else:
|
262
|
+
raise CommandError(f"Failed to set default serializer to {serializer_name}")
|
263
|
+
|
264
|
+
def handle_stats(self, options):
|
265
|
+
"""Show performance statistics."""
|
266
|
+
stats = get_performance_stats()
|
267
|
+
|
268
|
+
if options.get('json'):
|
269
|
+
self.stdout.write(json.dumps(stats, indent=2))
|
270
|
+
else:
|
271
|
+
self.display_stats(stats)
|
272
|
+
|
273
|
+
if options.get('clear'):
|
274
|
+
# Clear statistics if requested
|
275
|
+
manager = get_serializer_manager()
|
276
|
+
if hasattr(manager, 'reset_performance_stats'):
|
277
|
+
manager.reset_performance_stats()
|
278
|
+
self.stdout.write(
|
279
|
+
self.style.SUCCESS("Performance statistics cleared")
|
280
|
+
)
|
281
|
+
|
282
|
+
def handle_clear_cache(self, options):
|
283
|
+
"""Clear serializer caches."""
|
284
|
+
serializer_name = options.get('serializer')
|
285
|
+
|
286
|
+
if serializer_name:
|
287
|
+
clear_serializer_caches(serializer_name)
|
288
|
+
self.stdout.write(
|
289
|
+
self.style.SUCCESS(f"Cache cleared for {serializer_name}")
|
290
|
+
)
|
291
|
+
else:
|
292
|
+
clear_serializer_caches()
|
293
|
+
self.stdout.write(
|
294
|
+
self.style.SUCCESS("All serializer caches cleared")
|
295
|
+
)
|
296
|
+
|
297
|
+
def handle_health(self, options):
|
298
|
+
"""Run serializer health checks."""
|
299
|
+
model_name = options.get('model')
|
300
|
+
|
301
|
+
if model_name:
|
302
|
+
# Test specific model
|
303
|
+
model_class = self.get_model_class(model_name)
|
304
|
+
self.run_model_health_check(model_class)
|
305
|
+
else:
|
306
|
+
# Test all MojoModel subclasses
|
307
|
+
self.run_full_health_check()
|
308
|
+
|
309
|
+
def handle_test(self, options):
|
310
|
+
"""Test serializer functionality."""
|
311
|
+
model_class = self.get_model_class(options['model'])
|
312
|
+
graph = options['graph']
|
313
|
+
|
314
|
+
# Get a test instance
|
315
|
+
instance = model_class.objects.first()
|
316
|
+
if not instance:
|
317
|
+
raise CommandError(f"No instances found for model {model_class.__name__}")
|
318
|
+
|
319
|
+
self.stdout.write(
|
320
|
+
self.style.SUCCESS(f"Testing {model_class.__name__} serialization")
|
321
|
+
)
|
322
|
+
|
323
|
+
# Test each registered serializer
|
324
|
+
serializers = list_serializers()
|
325
|
+
manager = get_serializer_manager()
|
326
|
+
|
327
|
+
for serializer_name in serializers.keys():
|
328
|
+
self.stdout.write(f"\nTesting {serializer_name}:")
|
329
|
+
|
330
|
+
try:
|
331
|
+
# Test single instance
|
332
|
+
serializer = manager.get_serializer(instance, graph=graph, serializer_type=serializer_name)
|
333
|
+
data = serializer.serialize()
|
334
|
+
json_output = serializer.to_json()
|
335
|
+
|
336
|
+
self.stdout.write(
|
337
|
+
self.style.SUCCESS(f" ✓ Single instance: {len(str(data))} chars")
|
338
|
+
)
|
339
|
+
|
340
|
+
# Test queryset
|
341
|
+
queryset = model_class.objects.all()[:5] # Test with 5 instances
|
342
|
+
serializer = manager.get_serializer(queryset, graph=graph, serializer_type=serializer_name, many=True)
|
343
|
+
list_data = serializer.serialize()
|
344
|
+
list_json = serializer.to_json()
|
345
|
+
|
346
|
+
self.stdout.write(
|
347
|
+
self.style.SUCCESS(f" ✓ QuerySet ({len(list_data)} items): {len(str(list_json))} chars")
|
348
|
+
)
|
349
|
+
|
350
|
+
except Exception as e:
|
351
|
+
self.stdout.write(
|
352
|
+
self.style.ERROR(f" ✗ Failed: {str(e)}")
|
353
|
+
)
|
354
|
+
|
355
|
+
def get_model_class(self, model_string):
|
356
|
+
"""Get model class from string."""
|
357
|
+
try:
|
358
|
+
if '.' in model_string:
|
359
|
+
app_label, model_name = model_string.split('.', 1)
|
360
|
+
return apps.get_model(app_label, model_name)
|
361
|
+
else:
|
362
|
+
# Try to find model in any app
|
363
|
+
for app_config in apps.get_app_configs():
|
364
|
+
try:
|
365
|
+
return app_config.get_model(model_string)
|
366
|
+
except LookupError:
|
367
|
+
continue
|
368
|
+
raise CommandError(f"Model '{model_string}' not found")
|
369
|
+
except Exception as e:
|
370
|
+
raise CommandError(f"Invalid model specification '{model_string}': {str(e)}")
|
371
|
+
|
372
|
+
def display_benchmark_results(self, results):
|
373
|
+
"""Display benchmark results in a formatted table."""
|
374
|
+
if not results:
|
375
|
+
self.stdout.write(self.style.WARNING("No benchmark results"))
|
376
|
+
return
|
377
|
+
|
378
|
+
self.stdout.write("\nBenchmark Results:")
|
379
|
+
self.stdout.write("=" * 80)
|
380
|
+
|
381
|
+
# Table header
|
382
|
+
header = f"{'Serializer':<15} {'Avg Time':<12} {'Min Time':<12} {'Max Time':<12} {'Obj/Sec':<10}"
|
383
|
+
self.stdout.write(header)
|
384
|
+
self.stdout.write("-" * 80)
|
385
|
+
|
386
|
+
# Sort by average time
|
387
|
+
sorted_results = sorted(
|
388
|
+
results.items(),
|
389
|
+
key=lambda x: x[1].get('avg_time', float('inf'))
|
390
|
+
)
|
391
|
+
|
392
|
+
for name, stats in sorted_results:
|
393
|
+
if 'error' in stats:
|
394
|
+
row = f"{name:<15} {stats['error']:<50}"
|
395
|
+
self.stdout.write(self.style.ERROR(row))
|
396
|
+
else:
|
397
|
+
avg_time = f"{stats['avg_time']:.4f}s"
|
398
|
+
min_time = f"{stats['min_time']:.4f}s"
|
399
|
+
max_time = f"{stats['max_time']:.4f}s"
|
400
|
+
obj_per_sec = f"{stats.get('objects_per_second', 0):.1f}"
|
401
|
+
|
402
|
+
row = f"{name:<15} {avg_time:<12} {min_time:<12} {max_time:<12} {obj_per_sec:<10}"
|
403
|
+
self.stdout.write(row)
|
404
|
+
|
405
|
+
if stats.get('errors', 0) > 0:
|
406
|
+
self.stdout.write(
|
407
|
+
self.style.WARNING(f" └─ {stats['errors']} errors occurred")
|
408
|
+
)
|
409
|
+
|
410
|
+
def display_stats(self, stats):
|
411
|
+
"""Display performance statistics."""
|
412
|
+
self.stdout.write(self.style.SUCCESS("Serializer Performance Statistics:"))
|
413
|
+
|
414
|
+
# Default serializer
|
415
|
+
default_serializer = stats.get('default_serializer')
|
416
|
+
if default_serializer:
|
417
|
+
self.stdout.write(f"Default Serializer: {default_serializer}")
|
418
|
+
|
419
|
+
# Registered serializers
|
420
|
+
registered = stats.get('registered_serializers', {})
|
421
|
+
if registered:
|
422
|
+
self.stdout.write(f"Registered Serializers: {len(registered)}")
|
423
|
+
for name, info in registered.items():
|
424
|
+
status = " (default)" if info['is_default'] else ""
|
425
|
+
self.stdout.write(f" • {name}{status}")
|
426
|
+
|
427
|
+
# Usage statistics
|
428
|
+
usage_stats = stats.get('usage_stats', {})
|
429
|
+
if usage_stats:
|
430
|
+
self.stdout.write("\nUsage Statistics:")
|
431
|
+
for serializer, data in usage_stats.items():
|
432
|
+
self.stdout.write(
|
433
|
+
f" {serializer}: {data['count']} uses, "
|
434
|
+
f"{data['total_objects']} objects serialized"
|
435
|
+
)
|
436
|
+
|
437
|
+
# Individual serializer stats
|
438
|
+
for key, value in stats.items():
|
439
|
+
if key.endswith('_stats') and isinstance(value, dict):
|
440
|
+
serializer_name = key.replace('_stats', '')
|
441
|
+
self.stdout.write(f"\n{serializer_name.title()} Stats:")
|
442
|
+
for stat_key, stat_value in value.items():
|
443
|
+
self.stdout.write(f" {stat_key}: {stat_value}")
|
444
|
+
|
445
|
+
def save_benchmark_results(self, results, filename):
|
446
|
+
"""Save benchmark results to JSON file."""
|
447
|
+
try:
|
448
|
+
with open(filename, 'w') as f:
|
449
|
+
json.dump({
|
450
|
+
'timestamp': time.time(),
|
451
|
+
'results': results
|
452
|
+
}, f, indent=2)
|
453
|
+
self.stdout.write(
|
454
|
+
self.style.SUCCESS(f"Results saved to {filename}")
|
455
|
+
)
|
456
|
+
except Exception as e:
|
457
|
+
self.stdout.write(
|
458
|
+
self.style.ERROR(f"Failed to save results: {str(e)}")
|
459
|
+
)
|
460
|
+
|
461
|
+
def run_model_health_check(self, model_class):
|
462
|
+
"""Run health check for specific model."""
|
463
|
+
self.stdout.write(f"Health check for {model_class.__name__}:")
|
464
|
+
|
465
|
+
# Check if model has RestMeta
|
466
|
+
if hasattr(model_class, 'RestMeta'):
|
467
|
+
self.stdout.write(self.style.SUCCESS(" ✓ RestMeta found"))
|
468
|
+
|
469
|
+
# Check graphs
|
470
|
+
if hasattr(model_class.RestMeta, 'GRAPHS'):
|
471
|
+
graphs = model_class.RestMeta.GRAPHS
|
472
|
+
self.stdout.write(f" ✓ {len(graphs)} graphs configured: {list(graphs.keys())}")
|
473
|
+
else:
|
474
|
+
self.stdout.write(self.style.WARNING(" ⚠ No GRAPHS configured"))
|
475
|
+
else:
|
476
|
+
self.stdout.write(self.style.WARNING(" ⚠ No RestMeta found"))
|
477
|
+
|
478
|
+
# Test serialization if instances exist
|
479
|
+
if model_class.objects.exists():
|
480
|
+
instance = model_class.objects.first()
|
481
|
+
manager = get_serializer_manager()
|
482
|
+
|
483
|
+
try:
|
484
|
+
serializer = manager.get_serializer(instance)
|
485
|
+
data = serializer.serialize()
|
486
|
+
self.stdout.write(self.style.SUCCESS(" ✓ Serialization successful"))
|
487
|
+
except Exception as e:
|
488
|
+
self.stdout.write(self.style.ERROR(f" ✗ Serialization failed: {str(e)}"))
|
489
|
+
else:
|
490
|
+
self.stdout.write(self.style.WARNING(" ⚠ No instances available for testing"))
|
491
|
+
|
492
|
+
def run_full_health_check(self):
|
493
|
+
"""Run health check for all models."""
|
494
|
+
self.stdout.write("Running full serializer health check...")
|
495
|
+
|
496
|
+
# Import MojoModel to check for subclasses
|
497
|
+
try:
|
498
|
+
from mojo.models.rest import MojoModel
|
499
|
+
|
500
|
+
# Find all MojoModel subclasses
|
501
|
+
mojo_models = []
|
502
|
+
for app_config in apps.get_app_configs():
|
503
|
+
for model in app_config.get_models():
|
504
|
+
if hasattr(model, 'RestMeta') or 'MojoModel' in [c.__name__ for c in model.__mro__]:
|
505
|
+
mojo_models.append(model)
|
506
|
+
|
507
|
+
if not mojo_models:
|
508
|
+
self.stdout.write(self.style.WARNING("No MojoModel subclasses found"))
|
509
|
+
return
|
510
|
+
|
511
|
+
self.stdout.write(f"Found {len(mojo_models)} models to check\n")
|
512
|
+
|
513
|
+
for model in mojo_models:
|
514
|
+
self.run_model_health_check(model)
|
515
|
+
self.stdout.write("") # Empty line
|
516
|
+
|
517
|
+
except ImportError:
|
518
|
+
self.stdout.write(self.style.ERROR("Could not import MojoModel"))
|
519
|
+
|
520
|
+
def print_help(self):
|
521
|
+
"""Print command help."""
|
522
|
+
self.stdout.write("MOJO Serializer Administration Command")
|
523
|
+
self.stdout.write("\nAvailable actions:")
|
524
|
+
self.stdout.write(" list - List registered serializers")
|
525
|
+
self.stdout.write(" benchmark - Benchmark serializer performance")
|
526
|
+
self.stdout.write(" set-default - Set default serializer")
|
527
|
+
self.stdout.write(" stats - Show performance statistics")
|
528
|
+
self.stdout.write(" clear-cache - Clear serializer caches")
|
529
|
+
self.stdout.write(" health - Run health checks")
|
530
|
+
self.stdout.write(" test - Test serializer functionality")
|
531
|
+
self.stdout.write("\nUse 'python manage.py serializer_admin <action> --help' for more details")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-09 05:37
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('fileman', '0011_alter_filerendition_original_file'),
|
11
|
+
('account', '0003_group_mojo_secrets_user_mojo_secrets'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AddField(
|
16
|
+
model_name='user',
|
17
|
+
name='avatar',
|
18
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='fileman.file'),
|
19
|
+
),
|
20
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-08-26 16:05
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('account', '0004_user_avatar'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='group',
|
15
|
+
name='last_activity',
|
16
|
+
field=models.DateTimeField(db_index=True, default=None, null=True),
|
17
|
+
),
|
18
|
+
]
|
@@ -1,7 +1,11 @@
|
|
1
1
|
from django.db import models
|
2
2
|
from mojo.models import MojoModel, MojoSecrets
|
3
3
|
from mojo.helpers import dates
|
4
|
+
from mojo.apps import metrics
|
5
|
+
from mojo.helpers.settings import settings
|
4
6
|
|
7
|
+
GROUP_LAST_ACTIVITY_FREQ = settings.get("GROUP_LAST_ACTIVITY_FREQ", 300)
|
8
|
+
METRICS_TIMEZONE = settings.get("METRICS_TIMEZONE", "America/Los_Angeles")
|
5
9
|
|
6
10
|
|
7
11
|
class Group(MojoSecrets, MojoModel):
|
@@ -10,6 +14,7 @@ class Group(MojoSecrets, MojoModel):
|
|
10
14
|
"""
|
11
15
|
created = models.DateTimeField(auto_now_add=True, editable=False)
|
12
16
|
modified = models.DateTimeField(auto_now=True, db_index=True)
|
17
|
+
last_activity = models.DateTimeField(default=None, null=True, db_index=True)
|
13
18
|
|
14
19
|
name = models.CharField(max_length=200)
|
15
20
|
uuid = models.CharField(max_length=200, null=True, default=None, db_index=True)
|
@@ -36,6 +41,7 @@ class Group(MojoSecrets, MojoModel):
|
|
36
41
|
'name',
|
37
42
|
'created',
|
38
43
|
'modified',
|
44
|
+
'last_activity',
|
39
45
|
'is_active',
|
40
46
|
'kind',
|
41
47
|
]
|
@@ -46,15 +52,17 @@ class Group(MojoSecrets, MojoModel):
|
|
46
52
|
'name',
|
47
53
|
'created',
|
48
54
|
'modified',
|
55
|
+
'last_activity',
|
49
56
|
'is_active',
|
50
57
|
'kind',
|
51
58
|
'parent',
|
52
59
|
'metadata'
|
53
|
-
]
|
60
|
+
],
|
61
|
+
"graphs": {
|
62
|
+
"parent": "basic"
|
63
|
+
}
|
54
64
|
},
|
55
|
-
|
56
|
-
"parent": "basic"
|
57
|
-
}
|
65
|
+
|
58
66
|
}
|
59
67
|
|
60
68
|
@property
|
@@ -68,7 +76,7 @@ class Group(MojoSecrets, MojoModel):
|
|
68
76
|
return dates.get_local_time(self.timezone, dt_utc)
|
69
77
|
|
70
78
|
def __str__(self):
|
71
|
-
return self.name
|
79
|
+
return str(self.name)
|
72
80
|
|
73
81
|
def has_permission(self, user):
|
74
82
|
from mojo.account.models.member import GroupMember
|
@@ -82,6 +90,14 @@ class Group(MojoSecrets, MojoModel):
|
|
82
90
|
return ms.has_permission(perms)
|
83
91
|
return False
|
84
92
|
|
93
|
+
def touch(self):
|
94
|
+
# can't subtract offset-naive and offset-aware datetimes
|
95
|
+
if self.last_activity and not dates.is_today(self.last_activity, METRICS_TIMEZONE):
|
96
|
+
metrics.record("group_activity_day", category="group", min_granularity="days")
|
97
|
+
if self.last_activity is None or dates.has_time_elsapsed(self.last_activity, seconds=GROUP_LAST_ACTIVITY_FREQ):
|
98
|
+
self.last_activity = dates.utcnow()
|
99
|
+
self.atomic_save()
|
100
|
+
|
85
101
|
def get_metadata(self):
|
86
102
|
# converts our local metadata into an objict
|
87
103
|
self.metadata = self.jsonfield_as_objict("metadata")
|
@@ -94,5 +110,7 @@ class Group(MojoSecrets, MojoModel):
|
|
94
110
|
def on_rest_handle_list(cls, request):
|
95
111
|
if cls.rest_check_permission(request, "VIEW_PERMS"):
|
96
112
|
return cls.on_rest_list(request)
|
97
|
-
|
98
|
-
|
113
|
+
if getattr(request.user, 'members') is not None:
|
114
|
+
group_ids = request.user.members.values_list('group__id', flat=True)
|
115
|
+
return cls.on_rest_list(request, cls.objects.filter(id__in=group_ids))
|
116
|
+
return cls.on_rest_list(request, cls.objects.none())
|