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,454 @@
|
|
1
|
+
"""
|
2
|
+
Cache Utilities for Django-MOJO Serializers
|
3
|
+
|
4
|
+
Provides utility functions for cache key generation, TTL extraction from RestMeta.GRAPHS,
|
5
|
+
statistics collection, and cache management operations.
|
6
|
+
|
7
|
+
Key Features:
|
8
|
+
- Generate consistent cache keys for model instances
|
9
|
+
- Extract cache_ttl from RestMeta.GRAPHS configuration (defaults to 0)
|
10
|
+
- Collect statistics from all cache backends
|
11
|
+
- Provide cache management operations
|
12
|
+
- Thread-safe utility functions
|
13
|
+
"""
|
14
|
+
|
15
|
+
import time
|
16
|
+
from typing import Any, Optional, Dict, Union
|
17
|
+
from django.db.models import Model, QuerySet
|
18
|
+
|
19
|
+
# Use logit with graceful fallback
|
20
|
+
try:
|
21
|
+
from mojo.helpers import logit
|
22
|
+
logger = logit.get_logger("cache_utils", "cache_utils.log")
|
23
|
+
except Exception:
|
24
|
+
import logging
|
25
|
+
logger = logging.getLogger("cache_utils")
|
26
|
+
|
27
|
+
|
28
|
+
def get_cache_key(instance: Model, graph: str = "default") -> Optional[str]:
|
29
|
+
"""
|
30
|
+
Generate consistent cache key for model instance and graph.
|
31
|
+
|
32
|
+
Format: "ModelName_pk_graph"
|
33
|
+
Example: "Event_123_default", "User_456_list"
|
34
|
+
|
35
|
+
:param instance: Django model instance
|
36
|
+
:param graph: Graph configuration name
|
37
|
+
:return: Cache key string or None if instance has no PK
|
38
|
+
"""
|
39
|
+
# Fast check - pk should always exist on Django models
|
40
|
+
pk = instance.pk
|
41
|
+
if pk is None:
|
42
|
+
return None
|
43
|
+
|
44
|
+
# Use string concatenation for better performance than f-strings
|
45
|
+
return instance.__class__.__name__ + "_" + str(pk) + "_" + graph
|
46
|
+
|
47
|
+
|
48
|
+
def get_model_cache_ttl(instance: Model, graph: str = "default") -> int:
|
49
|
+
"""
|
50
|
+
Extract cache TTL from model's RestMeta.GRAPHS configuration.
|
51
|
+
|
52
|
+
Returns 0 (no caching) by default for safety.
|
53
|
+
|
54
|
+
:param instance: Django model instance
|
55
|
+
:param graph: Graph configuration name
|
56
|
+
:return: TTL in seconds (0 = no caching)
|
57
|
+
"""
|
58
|
+
# Fast path: check for RestMeta existence
|
59
|
+
rest_meta = getattr(instance, 'RestMeta', None)
|
60
|
+
if rest_meta is None:
|
61
|
+
return 0
|
62
|
+
|
63
|
+
# Fast path: check for GRAPHS
|
64
|
+
graphs = getattr(rest_meta, 'GRAPHS', None)
|
65
|
+
if graphs is None:
|
66
|
+
return 0
|
67
|
+
|
68
|
+
# Get specific graph configuration
|
69
|
+
graph_config = graphs.get(graph)
|
70
|
+
if graph_config is None:
|
71
|
+
# Try default graph as fallback
|
72
|
+
graph_config = graphs.get('default')
|
73
|
+
if graph_config is None:
|
74
|
+
return 0
|
75
|
+
|
76
|
+
# Extract cache_ttl, default to 0 for safety
|
77
|
+
ttl = graph_config.get('cache_ttl', 0)
|
78
|
+
|
79
|
+
# Ensure TTL is a valid integer
|
80
|
+
if not isinstance(ttl, int) or ttl < 0:
|
81
|
+
return 0
|
82
|
+
|
83
|
+
return ttl
|
84
|
+
|
85
|
+
|
86
|
+
def get_cache_stats() -> Dict[str, Any]:
|
87
|
+
"""
|
88
|
+
Get comprehensive cache statistics from all backends.
|
89
|
+
|
90
|
+
:return: Dictionary with cache statistics
|
91
|
+
"""
|
92
|
+
try:
|
93
|
+
from .backends import get_cache_backend
|
94
|
+
|
95
|
+
backend = get_cache_backend()
|
96
|
+
stats = backend.stats()
|
97
|
+
|
98
|
+
# Add timestamp for monitoring
|
99
|
+
stats['timestamp'] = time.time()
|
100
|
+
stats['timestamp_human'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
101
|
+
|
102
|
+
return stats
|
103
|
+
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Error getting cache statistics: {e}")
|
106
|
+
return {
|
107
|
+
'error': str(e),
|
108
|
+
'timestamp': time.time()
|
109
|
+
}
|
110
|
+
|
111
|
+
|
112
|
+
def clear_all_caches() -> bool:
|
113
|
+
"""
|
114
|
+
Clear all cached items from the cache backend.
|
115
|
+
|
116
|
+
:return: True if successful
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
from .backends import get_cache_backend, reset_cache_backend
|
120
|
+
|
121
|
+
backend = get_cache_backend()
|
122
|
+
result = backend.clear()
|
123
|
+
|
124
|
+
# Reset the backend to clear any in-memory state
|
125
|
+
reset_cache_backend()
|
126
|
+
|
127
|
+
logger.info("All serializer caches cleared")
|
128
|
+
return result
|
129
|
+
|
130
|
+
except Exception as e:
|
131
|
+
logger.error(f"Error clearing caches: {e}")
|
132
|
+
return False
|
133
|
+
|
134
|
+
|
135
|
+
def warm_cache(instances: Union[Model, QuerySet, list], graph: str = "default") -> Dict[str, Any]:
|
136
|
+
"""
|
137
|
+
Pre-warm the cache with serialized instances.
|
138
|
+
|
139
|
+
Useful for warming cache after deployments or during low-traffic periods.
|
140
|
+
|
141
|
+
:param instances: Model instance, QuerySet, or list of instances
|
142
|
+
:param graph: Graph configuration to use
|
143
|
+
:return: Dictionary with warming statistics
|
144
|
+
"""
|
145
|
+
from ..serializer import OptimizedGraphSerializer # Avoid circular import
|
146
|
+
|
147
|
+
start_time = time.time()
|
148
|
+
warmed_count = 0
|
149
|
+
error_count = 0
|
150
|
+
|
151
|
+
# Handle different input types
|
152
|
+
if isinstance(instances, Model):
|
153
|
+
instances = [instances]
|
154
|
+
elif isinstance(instances, QuerySet):
|
155
|
+
instances = list(instances)
|
156
|
+
|
157
|
+
try:
|
158
|
+
# Serialize each instance to warm the cache
|
159
|
+
for instance in instances:
|
160
|
+
try:
|
161
|
+
serializer = OptimizedGraphSerializer(instance, graph=graph)
|
162
|
+
serializer.serialize() # This will cache the result
|
163
|
+
warmed_count += 1
|
164
|
+
except Exception as e:
|
165
|
+
logger.warning(f"Error warming cache for {instance}: {e}")
|
166
|
+
error_count += 1
|
167
|
+
|
168
|
+
duration = time.time() - start_time
|
169
|
+
|
170
|
+
stats = {
|
171
|
+
'warmed_count': warmed_count,
|
172
|
+
'error_count': error_count,
|
173
|
+
'duration': duration,
|
174
|
+
'objects_per_second': warmed_count / duration if duration > 0 else 0,
|
175
|
+
'graph': graph
|
176
|
+
}
|
177
|
+
|
178
|
+
logger.info(f"Cache warming completed: {warmed_count} objects in {duration:.2f}s")
|
179
|
+
return stats
|
180
|
+
|
181
|
+
except Exception as e:
|
182
|
+
logger.error(f"Cache warming failed: {e}")
|
183
|
+
return {
|
184
|
+
'error': str(e),
|
185
|
+
'warmed_count': warmed_count,
|
186
|
+
'error_count': error_count
|
187
|
+
}
|
188
|
+
|
189
|
+
|
190
|
+
def invalidate_model_cache(model_class, instance_pk: Any = None, graph: str = None):
|
191
|
+
"""
|
192
|
+
Invalidate cache entries for a specific model.
|
193
|
+
|
194
|
+
:param model_class: Django model class
|
195
|
+
:param instance_pk: Specific instance PK to invalidate (None = all instances)
|
196
|
+
:param graph: Specific graph to invalidate (None = all graphs)
|
197
|
+
"""
|
198
|
+
try:
|
199
|
+
from .backends import get_cache_backend
|
200
|
+
|
201
|
+
backend = get_cache_backend()
|
202
|
+
model_name = model_class.__name__
|
203
|
+
|
204
|
+
# Get current cache stats to see what we're working with
|
205
|
+
stats = backend.stats()
|
206
|
+
if stats.get('current_size', 0) == 0:
|
207
|
+
return # Nothing to invalidate
|
208
|
+
|
209
|
+
deleted_count = 0
|
210
|
+
|
211
|
+
if instance_pk is not None and graph is not None:
|
212
|
+
# Invalidate specific instance + graph
|
213
|
+
cache_key = f"{model_name}_{instance_pk}_{graph}"
|
214
|
+
if backend.delete(cache_key):
|
215
|
+
deleted_count += 1
|
216
|
+
else:
|
217
|
+
# We need to iterate through cache to find matching keys
|
218
|
+
# Note: This is not efficient for large caches, but necessary without key scanning
|
219
|
+
logger.warning(f"Bulk invalidation not optimized for {model_name}")
|
220
|
+
# For now, just clear all caches
|
221
|
+
# TODO: Implement more efficient bulk invalidation when we add Redis
|
222
|
+
if backend.clear():
|
223
|
+
logger.info(f"Cleared all caches due to {model_name} invalidation")
|
224
|
+
|
225
|
+
if deleted_count > 0:
|
226
|
+
logger.info(f"Invalidated {deleted_count} cache entries for {model_name}")
|
227
|
+
|
228
|
+
except Exception as e:
|
229
|
+
logger.error(f"Error invalidating cache for {model_class.__name__}: {e}")
|
230
|
+
|
231
|
+
|
232
|
+
def get_cache_info() -> Dict[str, Any]:
|
233
|
+
"""
|
234
|
+
Get comprehensive cache information including configuration and status.
|
235
|
+
|
236
|
+
:return: Dictionary with cache information
|
237
|
+
"""
|
238
|
+
try:
|
239
|
+
from django.conf import settings
|
240
|
+
from .backends import get_cache_backend
|
241
|
+
|
242
|
+
# Get backend info
|
243
|
+
backend = get_cache_backend()
|
244
|
+
stats = backend.stats()
|
245
|
+
|
246
|
+
# Get configuration
|
247
|
+
cache_config = getattr(settings, 'MOJO_SERIALIZER_CACHE', {})
|
248
|
+
|
249
|
+
return {
|
250
|
+
'backend_type': stats.get('backend', 'unknown'),
|
251
|
+
'configuration': cache_config,
|
252
|
+
'statistics': stats,
|
253
|
+
'memory_usage_estimate': _estimate_memory_usage(stats),
|
254
|
+
'recommendations': _get_cache_recommendations(stats)
|
255
|
+
}
|
256
|
+
|
257
|
+
except Exception as e:
|
258
|
+
logger.error(f"Error getting cache info: {e}")
|
259
|
+
return {'error': str(e)}
|
260
|
+
|
261
|
+
|
262
|
+
def _estimate_memory_usage(stats: Dict[str, Any]) -> Dict[str, Any]:
|
263
|
+
"""
|
264
|
+
Estimate memory usage based on cache statistics.
|
265
|
+
|
266
|
+
:param stats: Cache statistics
|
267
|
+
:return: Memory usage estimates
|
268
|
+
"""
|
269
|
+
current_size = stats.get('current_size', 0)
|
270
|
+
|
271
|
+
if current_size == 0:
|
272
|
+
return {'estimated_mb': 0, 'estimated_bytes': 0}
|
273
|
+
|
274
|
+
# Rough estimate: 2KB per cached object (JSON + overhead)
|
275
|
+
estimated_bytes = current_size * 2048
|
276
|
+
estimated_mb = estimated_bytes / (1024 * 1024)
|
277
|
+
|
278
|
+
return {
|
279
|
+
'estimated_bytes': estimated_bytes,
|
280
|
+
'estimated_mb': round(estimated_mb, 2),
|
281
|
+
'objects': current_size,
|
282
|
+
'note': 'Rough estimate assuming ~2KB per cached object'
|
283
|
+
}
|
284
|
+
|
285
|
+
|
286
|
+
def _get_cache_recommendations(stats: Dict[str, Any]) -> list:
|
287
|
+
"""
|
288
|
+
Generate cache optimization recommendations based on statistics.
|
289
|
+
|
290
|
+
:param stats: Cache statistics
|
291
|
+
:return: List of recommendation strings
|
292
|
+
"""
|
293
|
+
recommendations = []
|
294
|
+
|
295
|
+
# Hit rate recommendations
|
296
|
+
hit_rate = stats.get('hit_rate', 0)
|
297
|
+
if hit_rate < 0.3:
|
298
|
+
recommendations.append("Low cache hit rate (<30%). Consider increasing cache_ttl values.")
|
299
|
+
elif hit_rate > 0.9:
|
300
|
+
recommendations.append("Excellent cache hit rate (>90%). Cache is working very well.")
|
301
|
+
|
302
|
+
# Eviction recommendations
|
303
|
+
evictions = stats.get('evictions', 0)
|
304
|
+
sets = stats.get('sets', 0)
|
305
|
+
if evictions > 0 and sets > 0:
|
306
|
+
eviction_rate = evictions / sets
|
307
|
+
if eviction_rate > 0.2:
|
308
|
+
recommendations.append(f"High eviction rate ({eviction_rate:.1%}). Consider increasing max_size.")
|
309
|
+
|
310
|
+
# Error recommendations
|
311
|
+
errors = stats.get('errors', 0)
|
312
|
+
if errors > 0:
|
313
|
+
recommendations.append(f"Cache errors detected ({errors}). Check logs for details.")
|
314
|
+
|
315
|
+
# Expired items
|
316
|
+
expired_items = stats.get('expired_items', 0)
|
317
|
+
total_requests = stats.get('total_requests', 0)
|
318
|
+
if expired_items > 0 and total_requests > 0:
|
319
|
+
expired_rate = expired_items / total_requests
|
320
|
+
if expired_rate > 0.1:
|
321
|
+
recommendations.append(f"High expiration rate ({expired_rate:.1%}). Consider longer cache_ttl values.")
|
322
|
+
|
323
|
+
if not recommendations:
|
324
|
+
recommendations.append("Cache performance looks good!")
|
325
|
+
|
326
|
+
return recommendations
|
327
|
+
|
328
|
+
|
329
|
+
def is_cacheable(instance: Model, graph: str = "default") -> bool:
|
330
|
+
"""
|
331
|
+
Check if an instance is cacheable based on its RestMeta configuration.
|
332
|
+
|
333
|
+
:param instance: Django model instance
|
334
|
+
:param graph: Graph configuration name
|
335
|
+
:return: True if cacheable (TTL > 0)
|
336
|
+
"""
|
337
|
+
return get_model_cache_ttl(instance, graph) > 0
|
338
|
+
|
339
|
+
|
340
|
+
def debug_cache_key(instance: Model, graph: str = "default") -> Dict[str, Any]:
|
341
|
+
"""
|
342
|
+
Debug information for cache key and configuration.
|
343
|
+
|
344
|
+
Useful for troubleshooting caching issues.
|
345
|
+
|
346
|
+
:param instance: Django model instance
|
347
|
+
:param graph: Graph configuration name
|
348
|
+
:return: Debug information dictionary
|
349
|
+
"""
|
350
|
+
try:
|
351
|
+
cache_key = get_cache_key(instance, graph)
|
352
|
+
ttl = get_model_cache_ttl(instance, graph)
|
353
|
+
|
354
|
+
# Check if already cached
|
355
|
+
from .backends import get_cache_backend
|
356
|
+
backend = get_cache_backend()
|
357
|
+
cached_value = backend.get(cache_key) if cache_key else None
|
358
|
+
|
359
|
+
return {
|
360
|
+
'instance': f"{instance.__class__.__name__}(pk={instance.pk})",
|
361
|
+
'graph': graph,
|
362
|
+
'cache_key': cache_key,
|
363
|
+
'ttl': ttl,
|
364
|
+
'is_cacheable': ttl > 0,
|
365
|
+
'is_cached': cached_value is not None,
|
366
|
+
'has_rest_meta': hasattr(instance, 'RestMeta'),
|
367
|
+
'has_graphs': hasattr(instance, 'RestMeta') and hasattr(instance.RestMeta, 'GRAPHS'),
|
368
|
+
'available_graphs': list(instance.RestMeta.GRAPHS.keys()) if hasattr(instance, 'RestMeta') and hasattr(instance.RestMeta, 'GRAPHS') else []
|
369
|
+
}
|
370
|
+
|
371
|
+
except Exception as e:
|
372
|
+
return {
|
373
|
+
'error': str(e),
|
374
|
+
'instance': str(instance),
|
375
|
+
'graph': graph
|
376
|
+
}
|
377
|
+
|
378
|
+
|
379
|
+
def test_json_performance(test_data: Any = None, iterations: int = 1000) -> Dict[str, Any]:
|
380
|
+
"""
|
381
|
+
Test JSON serialization performance comparing ujson vs standard json.
|
382
|
+
|
383
|
+
:param test_data: Data to serialize (uses default test data if None)
|
384
|
+
:param iterations: Number of iterations to run
|
385
|
+
:return: Performance comparison results
|
386
|
+
"""
|
387
|
+
import time
|
388
|
+
|
389
|
+
# Default test data if none provided
|
390
|
+
if test_data is None:
|
391
|
+
test_data = {
|
392
|
+
'id': 123,
|
393
|
+
'name': 'Test Object',
|
394
|
+
'created': time.time(),
|
395
|
+
'active': True,
|
396
|
+
'metadata': {
|
397
|
+
'type': 'performance_test',
|
398
|
+
'values': list(range(100)),
|
399
|
+
'nested': {
|
400
|
+
'deep': {
|
401
|
+
'data': 'test_value' * 10
|
402
|
+
}
|
403
|
+
}
|
404
|
+
}
|
405
|
+
}
|
406
|
+
|
407
|
+
results = {
|
408
|
+
'test_data_size': len(str(test_data)),
|
409
|
+
'iterations': iterations,
|
410
|
+
'ujson_available': False,
|
411
|
+
'standard_json_time': 0.0,
|
412
|
+
'ujson_time': 0.0,
|
413
|
+
'speedup_ratio': 1.0,
|
414
|
+
'recommendation': ''
|
415
|
+
}
|
416
|
+
|
417
|
+
# Test standard json
|
418
|
+
import json as std_json
|
419
|
+
start_time = time.perf_counter()
|
420
|
+
for _ in range(iterations):
|
421
|
+
json_str = std_json.dumps(test_data, default=str)
|
422
|
+
std_json.loads(json_str)
|
423
|
+
std_time = time.perf_counter() - start_time
|
424
|
+
results['standard_json_time'] = std_time
|
425
|
+
|
426
|
+
# Test ujson if available
|
427
|
+
try:
|
428
|
+
import ujson
|
429
|
+
results['ujson_available'] = True
|
430
|
+
|
431
|
+
start_time = time.perf_counter()
|
432
|
+
for _ in range(iterations):
|
433
|
+
json_str = ujson.dumps(test_data)
|
434
|
+
ujson.loads(json_str)
|
435
|
+
ujson_time = time.perf_counter() - start_time
|
436
|
+
results['ujson_time'] = ujson_time
|
437
|
+
|
438
|
+
# Calculate speedup
|
439
|
+
if ujson_time > 0:
|
440
|
+
results['speedup_ratio'] = std_time / ujson_time
|
441
|
+
if results['speedup_ratio'] > 2.0:
|
442
|
+
results['recommendation'] = f"ujson is {results['speedup_ratio']:.1f}x faster - consider using ujson for better performance"
|
443
|
+
else:
|
444
|
+
results['recommendation'] = f"ujson is {results['speedup_ratio']:.1f}x faster - moderate improvement"
|
445
|
+
else:
|
446
|
+
results['recommendation'] = "ujson time measurement failed"
|
447
|
+
|
448
|
+
except ImportError:
|
449
|
+
results['ujson_available'] = False
|
450
|
+
results['ujson_time'] = 0
|
451
|
+
results['speedup_ratio'] = 1.0
|
452
|
+
results['recommendation'] = "ujson not available - install with 'pip install ujson' for better performance"
|
453
|
+
|
454
|
+
return results
|
@@ -40,10 +40,18 @@ logger = logit.get_logger("serializer_manager", "serializer_manager.log")
|
|
40
40
|
_registry_lock = RLock()
|
41
41
|
|
42
42
|
# Default serializer configurations
|
43
|
+
# DEFAULT_SERIALIZERS = {
|
44
|
+
# 'simple': 'mojo.serializers.simple.GraphSerializer',
|
45
|
+
# 'optimized': 'mojo.serializers.core.serializer.OptimizedGraphSerializer',
|
46
|
+
# 'advanced': 'mojo.serializers.advanced.AdvancedGraphSerializer',
|
47
|
+
# }
|
48
|
+
|
43
49
|
DEFAULT_SERIALIZERS = {
|
44
|
-
'
|
45
|
-
|
46
|
-
|
50
|
+
'optimized': 'mojo.serializers.core.serializer.OptimizedGraphSerializer'
|
51
|
+
}
|
52
|
+
|
53
|
+
FORMAT_SERIALIZERS = {
|
54
|
+
'csv': 'mojo.serializers.formats.csv.CsvFormatter'
|
47
55
|
}
|
48
56
|
|
49
57
|
# Global serializer registry
|
@@ -58,7 +66,15 @@ _PERFORMANCE_DATA = {
|
|
58
66
|
|
59
67
|
|
60
68
|
class SerializerRegistry:
|
61
|
-
"""
|
69
|
+
"""
|
70
|
+
Registry for managing available serializers.
|
71
|
+
|
72
|
+
This registry supports lazy loading of serializers. When a serializer is
|
73
|
+
registered using an import path string, it is only imported when the
|
74
|
+
`register` method is called. The default serializers are registered
|
75
|
+
on the first call to `get_serializer_manager()`, avoiding imports at
|
76
|
+
application startup.
|
77
|
+
"""
|
62
78
|
|
63
79
|
def __init__(self):
|
64
80
|
self.serializers = {}
|
@@ -152,6 +168,7 @@ class SerializerManager:
|
|
152
168
|
self.default_serializer = default_serializer
|
153
169
|
self.performance_tracking = enable_performance_tracking
|
154
170
|
self.registry = _registry
|
171
|
+
self.serializer_class = None
|
155
172
|
|
156
173
|
# Initialize default serializers if not already done
|
157
174
|
self._ensure_default_serializers()
|
@@ -168,6 +185,12 @@ class SerializerManager:
|
|
168
185
|
serializer_class_or_path=import_path,
|
169
186
|
is_default=(name == 'optimized') # Set optimized as default
|
170
187
|
)
|
188
|
+
for format, import_path in FORMAT_SERIALIZERS.items():
|
189
|
+
_registry.register(
|
190
|
+
name=format,
|
191
|
+
serializer_class_or_path=import_path
|
192
|
+
)
|
193
|
+
|
171
194
|
|
172
195
|
def _load_configuration(self):
|
173
196
|
"""Load configuration from Django settings."""
|
@@ -193,6 +216,13 @@ class SerializerManager:
|
|
193
216
|
|
194
217
|
def get_serializer(self, instance, graph: str = "default", many: bool = None,
|
195
218
|
serializer_type: str = None, **kwargs):
|
219
|
+
if not self.serializer_class:
|
220
|
+
self.serializer_class = self.registry.get("optimized")
|
221
|
+
return self.serializer_class(instance, graph=graph, many=many, **kwargs)
|
222
|
+
|
223
|
+
|
224
|
+
def get_serializer_old(self, instance, graph: str = "default", many: bool = None,
|
225
|
+
serializer_type: str = None, **kwargs):
|
196
226
|
"""
|
197
227
|
Get appropriate serializer for the given instance and parameters.
|
198
228
|
|
@@ -237,6 +267,12 @@ class SerializerManager:
|
|
237
267
|
return fallback_class(instance, graph=graph, many=many)
|
238
268
|
raise
|
239
269
|
|
270
|
+
def get_format_serializer(self, format: str):
|
271
|
+
SerializerClass = self.registry.get(format)
|
272
|
+
if SerializerClass:
|
273
|
+
return SerializerClass()
|
274
|
+
raise ValueError(f"Serializer for format '{format}' not found")
|
275
|
+
|
240
276
|
def serialize(self, instance, graph: str = "default", many: bool = None,
|
241
277
|
serializer_type: str = None, **kwargs):
|
242
278
|
"""
|
@@ -499,3 +535,16 @@ def clear_serializer_caches(serializer_type: str = None):
|
|
499
535
|
def benchmark_serializers(instance, graph: str = "default", serializer_types: List[str] = None, iterations: int = 10):
|
500
536
|
"""Benchmark serializers globally."""
|
501
537
|
return get_serializer_manager().benchmark_serializers(instance, graph, serializer_types, iterations)
|
538
|
+
|
539
|
+
def list_serializers():
|
540
|
+
"""List all registered serializers globally."""
|
541
|
+
return get_serializer_manager().registry.list_serializers()
|
542
|
+
|
543
|
+
# Import ujson availability info
|
544
|
+
try:
|
545
|
+
import ujson
|
546
|
+
HAS_UJSON = True
|
547
|
+
UJSON_VERSION = getattr(ujson, '__version__', 'unknown')
|
548
|
+
except ImportError:
|
549
|
+
HAS_UJSON = False
|
550
|
+
UJSON_VERSION = None
|