django-nativemojo 0.1.10__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.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- 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 +651 -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/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 +319 -15
- mojo/apps/account/models/member.py +29 -5
- 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 +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- 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 +7 -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/s3.py +64 -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/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 +409 -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 +240 -58
- mojo/apps/fileman/models/manager.py +427 -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/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 +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- 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/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- 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 +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -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 +14 -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/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- 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/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- 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/bounce.py +0 -0
- 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 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- 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.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,518 @@
|
|
1
|
+
"""
|
2
|
+
Cache Backend Factory for Django-MOJO Serializers
|
3
|
+
|
4
|
+
Provides factory functions to create appropriate cache backends based on configuration.
|
5
|
+
Handles backend selection, configuration validation, and graceful fallback behavior.
|
6
|
+
|
7
|
+
Supported Backends:
|
8
|
+
- memory: In-memory LRU cache with TTL support
|
9
|
+
- redis: Distributed Redis-based caching
|
10
|
+
- disabled: No-op cache for development/testing
|
11
|
+
|
12
|
+
Factory Functions:
|
13
|
+
- get_cache_backend(): Get configured cache backend instance
|
14
|
+
- create_cache_backend(): Create specific backend type
|
15
|
+
- validate_cache_config(): Validate configuration parameters
|
16
|
+
|
17
|
+
Configuration via Django Settings:
|
18
|
+
MOJO_SERIALIZER_CACHE = {
|
19
|
+
'backend': 'memory', # 'memory', 'redis', 'disabled'
|
20
|
+
'memory': {
|
21
|
+
'max_size': 5000,
|
22
|
+
'enable_stats': True
|
23
|
+
},
|
24
|
+
'redis': {
|
25
|
+
'host': 'localhost',
|
26
|
+
'port': 6379,
|
27
|
+
'db': 2,
|
28
|
+
'key_prefix': 'mojo:serializer:'
|
29
|
+
}
|
30
|
+
}
|
31
|
+
"""
|
32
|
+
|
33
|
+
import threading
|
34
|
+
from typing import Dict, Any, Optional
|
35
|
+
from django.conf import settings
|
36
|
+
|
37
|
+
# Use logit with graceful fallback
|
38
|
+
try:
|
39
|
+
from mojo.helpers import logit
|
40
|
+
logger = logit.get_logger("cache_backends", "cache_backends.log")
|
41
|
+
except Exception:
|
42
|
+
import logging
|
43
|
+
logger = logging.getLogger("cache_backends")
|
44
|
+
|
45
|
+
from .base import CacheBackend
|
46
|
+
from .memory import MemoryCacheBackend
|
47
|
+
from .disabled import DisabledCacheBackend
|
48
|
+
|
49
|
+
# Check ujson availability for optimal JSON performance
|
50
|
+
try:
|
51
|
+
import ujson
|
52
|
+
HAS_UJSON = True
|
53
|
+
UJSON_VERSION = getattr(ujson, '__version__', 'unknown')
|
54
|
+
except ImportError:
|
55
|
+
ujson = None
|
56
|
+
HAS_UJSON = False
|
57
|
+
UJSON_VERSION = None
|
58
|
+
|
59
|
+
# Redis backend with graceful import fallback
|
60
|
+
try:
|
61
|
+
from .redis import RedisCacheBackend
|
62
|
+
HAS_REDIS_BACKEND = True
|
63
|
+
except ImportError:
|
64
|
+
RedisCacheBackend = None
|
65
|
+
HAS_REDIS_BACKEND = False
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
# Global cache backend instance (lazy initialization)
|
70
|
+
_cache_backend_instance = None
|
71
|
+
_backend_lock = threading.RLock()
|
72
|
+
|
73
|
+
# Default configuration
|
74
|
+
DEFAULT_CACHE_CONFIG = {
|
75
|
+
'backend': 'memory',
|
76
|
+
'memory': {
|
77
|
+
'max_size': 5000,
|
78
|
+
'enable_stats': True
|
79
|
+
},
|
80
|
+
'redis': {
|
81
|
+
'host': 'localhost',
|
82
|
+
'port': 6379,
|
83
|
+
'db': 0,
|
84
|
+
'key_prefix': 'mojo:serializer:',
|
85
|
+
'socket_timeout': 1.0,
|
86
|
+
'socket_connect_timeout': 1.0,
|
87
|
+
'enable_stats': True
|
88
|
+
},
|
89
|
+
'disabled': {}
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
def get_cache_backend() -> CacheBackend:
|
94
|
+
"""
|
95
|
+
Get the configured cache backend instance.
|
96
|
+
|
97
|
+
Uses lazy initialization and returns the same instance across calls.
|
98
|
+
Configuration is read from Django settings with sensible defaults.
|
99
|
+
Thread-safe singleton pattern.
|
100
|
+
|
101
|
+
:return: Configured cache backend instance
|
102
|
+
"""
|
103
|
+
global _cache_backend_instance
|
104
|
+
|
105
|
+
with _backend_lock:
|
106
|
+
if _cache_backend_instance is None:
|
107
|
+
_cache_backend_instance = create_cache_backend()
|
108
|
+
|
109
|
+
return _cache_backend_instance
|
110
|
+
|
111
|
+
|
112
|
+
def create_cache_backend(backend_type: Optional[str] = None, **kwargs) -> CacheBackend:
|
113
|
+
"""
|
114
|
+
Create cache backend instance based on type and configuration.
|
115
|
+
|
116
|
+
:param backend_type: Backend type ('memory', 'redis', 'disabled') or None for auto-detection
|
117
|
+
:param kwargs: Override configuration parameters
|
118
|
+
:return: Cache backend instance
|
119
|
+
"""
|
120
|
+
# Get configuration from Django settings
|
121
|
+
cache_config = getattr(settings, 'MOJO_SERIALIZER_CACHE', {})
|
122
|
+
config = {**DEFAULT_CACHE_CONFIG, **cache_config}
|
123
|
+
|
124
|
+
# Determine backend type
|
125
|
+
if backend_type is None:
|
126
|
+
backend_type = config.get('backend', 'memory').lower()
|
127
|
+
|
128
|
+
# Override with any provided kwargs
|
129
|
+
if kwargs:
|
130
|
+
config.update(kwargs)
|
131
|
+
|
132
|
+
# Validate configuration
|
133
|
+
validated_config = validate_cache_config(config, backend_type)
|
134
|
+
|
135
|
+
# Create appropriate backend
|
136
|
+
if backend_type == 'disabled':
|
137
|
+
logger.info("Creating disabled cache backend")
|
138
|
+
return DisabledCacheBackend()
|
139
|
+
|
140
|
+
elif backend_type == 'redis':
|
141
|
+
return _create_redis_backend(validated_config)
|
142
|
+
|
143
|
+
elif backend_type == 'memory':
|
144
|
+
return _create_memory_backend(validated_config)
|
145
|
+
|
146
|
+
else:
|
147
|
+
logger.warning(f"Unknown cache backend type '{backend_type}', falling back to memory")
|
148
|
+
return _create_memory_backend(validated_config)
|
149
|
+
|
150
|
+
|
151
|
+
def _create_redis_backend(config: Dict[str, Any]) -> CacheBackend:
|
152
|
+
"""
|
153
|
+
Create Redis cache backend with configuration validation and fallback.
|
154
|
+
"""
|
155
|
+
if not HAS_REDIS_BACKEND:
|
156
|
+
logger.warning(
|
157
|
+
"Redis backend requested but redis package not available. "
|
158
|
+
"Install with: pip install redis. Falling back to memory cache."
|
159
|
+
)
|
160
|
+
return _create_memory_backend(config)
|
161
|
+
|
162
|
+
redis_config = config.get('redis', {})
|
163
|
+
|
164
|
+
try:
|
165
|
+
logger.info(f"Creating Redis cache backend: {redis_config.get('host', 'localhost')}:{redis_config.get('port', 6379)}")
|
166
|
+
if HAS_UJSON:
|
167
|
+
logger.info(f"Using ujson {UJSON_VERSION} for optimal JSON performance")
|
168
|
+
else:
|
169
|
+
logger.warning("ujson not available - using standard json (slower performance)")
|
170
|
+
|
171
|
+
backend = RedisCacheBackend(**redis_config)
|
172
|
+
|
173
|
+
# Test connection
|
174
|
+
if not backend.ping():
|
175
|
+
raise ConnectionError("Redis ping failed")
|
176
|
+
|
177
|
+
logger.info("Redis cache backend successfully initialized")
|
178
|
+
return backend
|
179
|
+
|
180
|
+
except Exception as e:
|
181
|
+
logger.error(f"Failed to create Redis cache backend: {e}")
|
182
|
+
logger.info("Falling back to memory cache backend")
|
183
|
+
return _create_memory_backend(config)
|
184
|
+
|
185
|
+
|
186
|
+
def _create_memory_backend(config: Dict[str, Any]) -> CacheBackend:
|
187
|
+
"""
|
188
|
+
Create memory cache backend with configuration.
|
189
|
+
"""
|
190
|
+
memory_config = config.get('memory', {})
|
191
|
+
max_size = memory_config.get('max_size', 5000)
|
192
|
+
enable_stats = memory_config.get('enable_stats', True)
|
193
|
+
|
194
|
+
logger.info(f"Creating memory cache backend with max_size={max_size}")
|
195
|
+
if HAS_UJSON:
|
196
|
+
logger.info(f"Using ujson {UJSON_VERSION} for optimal JSON performance")
|
197
|
+
else:
|
198
|
+
logger.warning("ujson not available - using standard json (slower performance)")
|
199
|
+
|
200
|
+
return MemoryCacheBackend(max_size=max_size, enable_stats=enable_stats)
|
201
|
+
|
202
|
+
|
203
|
+
def validate_cache_config(config: Dict[str, Any], backend_type: str) -> Dict[str, Any]:
|
204
|
+
"""
|
205
|
+
Validate and normalize cache configuration.
|
206
|
+
|
207
|
+
:param config: Raw configuration dictionary
|
208
|
+
:param backend_type: Backend type to validate for
|
209
|
+
:return: Validated configuration with defaults applied
|
210
|
+
"""
|
211
|
+
validated = config.copy()
|
212
|
+
|
213
|
+
# Validate backend type
|
214
|
+
valid_backends = ['memory', 'redis', 'disabled']
|
215
|
+
if backend_type not in valid_backends:
|
216
|
+
logger.warning(f"Invalid backend type '{backend_type}', using 'memory'")
|
217
|
+
validated['backend'] = 'memory'
|
218
|
+
backend_type = 'memory'
|
219
|
+
|
220
|
+
# Backend-specific validation
|
221
|
+
if backend_type == 'memory':
|
222
|
+
memory_config = validated.setdefault('memory', {})
|
223
|
+
memory_config.setdefault('max_size', 5000)
|
224
|
+
memory_config.setdefault('enable_stats', True)
|
225
|
+
|
226
|
+
# Validate max_size
|
227
|
+
max_size = memory_config.get('max_size')
|
228
|
+
if not isinstance(max_size, int) or max_size <= 0:
|
229
|
+
logger.warning(f"Invalid memory max_size '{max_size}', using 5000")
|
230
|
+
memory_config['max_size'] = 5000
|
231
|
+
|
232
|
+
elif backend_type == 'redis':
|
233
|
+
redis_config = validated.setdefault('redis', {})
|
234
|
+
|
235
|
+
# Set defaults
|
236
|
+
redis_config.setdefault('host', 'localhost')
|
237
|
+
redis_config.setdefault('port', 6379)
|
238
|
+
redis_config.setdefault('db', 0)
|
239
|
+
redis_config.setdefault('key_prefix', 'mojo:serializer:')
|
240
|
+
redis_config.setdefault('socket_timeout', 1.0)
|
241
|
+
redis_config.setdefault('socket_connect_timeout', 1.0)
|
242
|
+
redis_config.setdefault('enable_stats', True)
|
243
|
+
|
244
|
+
# Validate port
|
245
|
+
port = redis_config.get('port')
|
246
|
+
if not isinstance(port, int) or port <= 0 or port > 65535:
|
247
|
+
logger.warning(f"Invalid Redis port '{port}', using 6379")
|
248
|
+
redis_config['port'] = 6379
|
249
|
+
|
250
|
+
# Validate db
|
251
|
+
db = redis_config.get('db')
|
252
|
+
if not isinstance(db, int) or db < 0:
|
253
|
+
logger.warning(f"Invalid Redis db '{db}', using 0")
|
254
|
+
redis_config['db'] = 0
|
255
|
+
|
256
|
+
# Validate timeouts
|
257
|
+
for timeout_key in ['socket_timeout', 'socket_connect_timeout']:
|
258
|
+
timeout = redis_config.get(timeout_key)
|
259
|
+
if not isinstance(timeout, (int, float)) or timeout <= 0:
|
260
|
+
logger.warning(f"Invalid Redis {timeout_key} '{timeout}', using 1.0")
|
261
|
+
redis_config[timeout_key] = 1.0
|
262
|
+
|
263
|
+
# Ensure key_prefix ends with colon
|
264
|
+
key_prefix = redis_config.get('key_prefix', '')
|
265
|
+
if key_prefix and not key_prefix.endswith(':'):
|
266
|
+
redis_config['key_prefix'] = f"{key_prefix}:"
|
267
|
+
|
268
|
+
return validated
|
269
|
+
|
270
|
+
|
271
|
+
def reset_cache_backend():
|
272
|
+
"""
|
273
|
+
Reset the global cache backend instance.
|
274
|
+
|
275
|
+
Forces recreation of the backend on next access. Useful for testing
|
276
|
+
or when configuration changes require a fresh backend instance.
|
277
|
+
"""
|
278
|
+
global _cache_backend_instance
|
279
|
+
|
280
|
+
with _backend_lock:
|
281
|
+
if _cache_backend_instance:
|
282
|
+
try:
|
283
|
+
_cache_backend_instance.clear()
|
284
|
+
if hasattr(_cache_backend_instance, 'close'):
|
285
|
+
_cache_backend_instance.close()
|
286
|
+
except Exception as e:
|
287
|
+
logger.warning(f"Error cleaning up old cache backend: {e}")
|
288
|
+
|
289
|
+
_cache_backend_instance = None
|
290
|
+
logger.info("Cache backend reset - will be recreated on next access")
|
291
|
+
|
292
|
+
|
293
|
+
def get_available_backends() -> Dict[str, Dict[str, Any]]:
|
294
|
+
"""
|
295
|
+
Get information about available cache backends.
|
296
|
+
|
297
|
+
:return: Dictionary with backend availability and capabilities
|
298
|
+
"""
|
299
|
+
backends = {
|
300
|
+
'memory': {
|
301
|
+
'available': True,
|
302
|
+
'description': 'In-memory LRU cache with TTL support',
|
303
|
+
'features': ['LRU eviction', 'TTL support', 'Thread-safe', 'Statistics'],
|
304
|
+
'suitable_for': ['development', 'single-server', 'testing']
|
305
|
+
},
|
306
|
+
'disabled': {
|
307
|
+
'available': True,
|
308
|
+
'description': 'No-op cache that disables caching',
|
309
|
+
'features': ['Zero overhead', 'Debug-friendly'],
|
310
|
+
'suitable_for': ['development', 'testing', 'debugging']
|
311
|
+
},
|
312
|
+
'redis': {
|
313
|
+
'available': HAS_REDIS_BACKEND,
|
314
|
+
'description': 'Distributed Redis-based caching',
|
315
|
+
'features': ['Distributed', 'Persistent', 'Native TTL', 'High performance'],
|
316
|
+
'suitable_for': ['production', 'multi-server', 'high-availability'],
|
317
|
+
'requirements': ['redis package', 'Redis server'],
|
318
|
+
'ujson_available': HAS_UJSON,
|
319
|
+
'ujson_version': UJSON_VERSION
|
320
|
+
}
|
321
|
+
}
|
322
|
+
|
323
|
+
if not HAS_REDIS_BACKEND:
|
324
|
+
backends['redis']['error'] = 'Redis package not installed'
|
325
|
+
|
326
|
+
# Add ujson info to memory backend as well
|
327
|
+
backends['memory']['ujson_available'] = HAS_UJSON
|
328
|
+
backends['memory']['ujson_version'] = UJSON_VERSION
|
329
|
+
|
330
|
+
# Add performance note
|
331
|
+
if not HAS_UJSON:
|
332
|
+
performance_note = 'Install ujson for 2-5x faster JSON serialization: pip install ujson'
|
333
|
+
backends['memory']['performance_note'] = performance_note
|
334
|
+
backends['redis']['performance_note'] = performance_note
|
335
|
+
|
336
|
+
return backends
|
337
|
+
|
338
|
+
|
339
|
+
def test_backend_connectivity(backend_type: str, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
340
|
+
"""
|
341
|
+
Test connectivity and functionality of a cache backend.
|
342
|
+
|
343
|
+
:param backend_type: Backend type to test
|
344
|
+
:param config: Optional configuration override
|
345
|
+
:return: Test results dictionary
|
346
|
+
"""
|
347
|
+
test_results = {
|
348
|
+
'backend': backend_type,
|
349
|
+
'available': False,
|
350
|
+
'connectivity': False,
|
351
|
+
'functionality': False,
|
352
|
+
'performance': {},
|
353
|
+
'errors': []
|
354
|
+
}
|
355
|
+
|
356
|
+
try:
|
357
|
+
# Create temporary backend instance
|
358
|
+
if config:
|
359
|
+
backend = create_cache_backend(backend_type, **config)
|
360
|
+
else:
|
361
|
+
# Use existing backend if it matches type
|
362
|
+
existing_backend = get_cache_backend()
|
363
|
+
if existing_backend.stats().get('backend') == backend_type:
|
364
|
+
backend = existing_backend
|
365
|
+
else:
|
366
|
+
backend = create_cache_backend(backend_type)
|
367
|
+
|
368
|
+
test_results['available'] = True
|
369
|
+
|
370
|
+
# Test connectivity
|
371
|
+
if hasattr(backend, 'ping'):
|
372
|
+
test_results['connectivity'] = backend.ping()
|
373
|
+
else:
|
374
|
+
test_results['connectivity'] = True
|
375
|
+
|
376
|
+
if test_results['connectivity']:
|
377
|
+
# Test basic functionality
|
378
|
+
import time
|
379
|
+
test_key = f"test_key_{int(time.time())}"
|
380
|
+
test_value = {'test': True, 'timestamp': time.time()}
|
381
|
+
|
382
|
+
# Test set/get cycle
|
383
|
+
start_time = time.perf_counter()
|
384
|
+
set_success = backend.set(test_key, test_value, ttl=60)
|
385
|
+
set_time = time.perf_counter() - start_time
|
386
|
+
|
387
|
+
if set_success:
|
388
|
+
start_time = time.perf_counter()
|
389
|
+
retrieved_value = backend.get(test_key)
|
390
|
+
get_time = time.perf_counter() - start_time
|
391
|
+
|
392
|
+
if retrieved_value == test_value:
|
393
|
+
test_results['functionality'] = True
|
394
|
+
test_results['performance'] = {
|
395
|
+
'set_time_ms': round(set_time * 1000, 2),
|
396
|
+
'get_time_ms': round(get_time * 1000, 2)
|
397
|
+
}
|
398
|
+
else:
|
399
|
+
test_results['errors'].append("Retrieved value doesn't match set value")
|
400
|
+
|
401
|
+
# Cleanup
|
402
|
+
backend.delete(test_key)
|
403
|
+
else:
|
404
|
+
test_results['errors'].append("Failed to set test value")
|
405
|
+
else:
|
406
|
+
test_results['errors'].append("Backend connectivity test failed")
|
407
|
+
|
408
|
+
except Exception as e:
|
409
|
+
test_results['errors'].append(f"Backend test failed: {str(e)}")
|
410
|
+
logger.error(f"Error testing {backend_type} backend: {e}")
|
411
|
+
|
412
|
+
return test_results
|
413
|
+
|
414
|
+
|
415
|
+
def get_cache_health() -> Dict[str, Any]:
|
416
|
+
"""
|
417
|
+
Get comprehensive cache system health information.
|
418
|
+
|
419
|
+
:return: Health status dictionary
|
420
|
+
"""
|
421
|
+
try:
|
422
|
+
backend = get_cache_backend()
|
423
|
+
stats = backend.stats()
|
424
|
+
|
425
|
+
# Calculate health score based on various metrics
|
426
|
+
health_score = 100
|
427
|
+
issues = []
|
428
|
+
|
429
|
+
# Check hit rate
|
430
|
+
hit_rate = stats.get('hit_rate', 0)
|
431
|
+
if hit_rate < 0.5:
|
432
|
+
health_score -= 20
|
433
|
+
issues.append(f"Low cache hit rate: {hit_rate:.1%}")
|
434
|
+
|
435
|
+
# Check error rate
|
436
|
+
errors = stats.get('errors', 0)
|
437
|
+
total_ops = stats.get('sets', 0) + stats.get('hits', 0) + stats.get('misses', 0)
|
438
|
+
if total_ops > 0:
|
439
|
+
error_rate = errors / total_ops
|
440
|
+
if error_rate > 0.05: # 5% error rate
|
441
|
+
health_score -= 30
|
442
|
+
issues.append(f"High error rate: {error_rate:.1%}")
|
443
|
+
|
444
|
+
# Check connectivity for Redis
|
445
|
+
if stats.get('backend') == 'redis':
|
446
|
+
if hasattr(backend, 'ping') and not backend.ping():
|
447
|
+
health_score -= 50
|
448
|
+
issues.append("Redis connectivity issues")
|
449
|
+
|
450
|
+
# Determine overall health status
|
451
|
+
if health_score >= 90:
|
452
|
+
status = 'excellent'
|
453
|
+
elif health_score >= 70:
|
454
|
+
status = 'good'
|
455
|
+
elif health_score >= 50:
|
456
|
+
status = 'fair'
|
457
|
+
else:
|
458
|
+
status = 'poor'
|
459
|
+
|
460
|
+
return {
|
461
|
+
'status': status,
|
462
|
+
'health_score': health_score,
|
463
|
+
'backend_type': stats.get('backend', 'unknown'),
|
464
|
+
'statistics': stats,
|
465
|
+
'issues': issues,
|
466
|
+
'recommendations': _get_health_recommendations(stats, issues)
|
467
|
+
}
|
468
|
+
|
469
|
+
except Exception as e:
|
470
|
+
logger.error(f"Error getting cache health: {e}")
|
471
|
+
return {
|
472
|
+
'status': 'error',
|
473
|
+
'health_score': 0,
|
474
|
+
'error': str(e)
|
475
|
+
}
|
476
|
+
|
477
|
+
|
478
|
+
def _get_health_recommendations(stats: Dict[str, Any], issues: list) -> list:
|
479
|
+
"""
|
480
|
+
Generate health recommendations based on statistics and issues.
|
481
|
+
"""
|
482
|
+
recommendations = []
|
483
|
+
|
484
|
+
# Hit rate recommendations
|
485
|
+
hit_rate = stats.get('hit_rate', 0)
|
486
|
+
if hit_rate < 0.3:
|
487
|
+
recommendations.append("Consider increasing cache TTL values in RestMeta.GRAPHS")
|
488
|
+
elif hit_rate < 0.5:
|
489
|
+
recommendations.append("Review caching strategy - some models may benefit from longer TTL")
|
490
|
+
|
491
|
+
# Memory recommendations for memory backend
|
492
|
+
if stats.get('backend') == 'memory':
|
493
|
+
utilization = stats.get('utilization', 0)
|
494
|
+
if utilization > 0.9:
|
495
|
+
recommendations.append("Memory cache is near capacity - consider increasing max_size")
|
496
|
+
|
497
|
+
evictions = stats.get('evictions', 0)
|
498
|
+
if evictions > 100:
|
499
|
+
recommendations.append("High eviction count - consider increasing cache size")
|
500
|
+
|
501
|
+
# Redis-specific recommendations
|
502
|
+
elif stats.get('backend') == 'redis':
|
503
|
+
if 'Redis connectivity issues' in issues:
|
504
|
+
recommendations.append("Check Redis server status and network connectivity")
|
505
|
+
|
506
|
+
redis_evictions = stats.get('redis_evicted_keys', 0)
|
507
|
+
if redis_evictions > 0:
|
508
|
+
recommendations.append("Redis is evicting keys - consider increasing Redis maxmemory")
|
509
|
+
|
510
|
+
# General recommendations
|
511
|
+
errors = stats.get('errors', 0)
|
512
|
+
if errors > 10:
|
513
|
+
recommendations.append("Multiple cache errors detected - check logs for details")
|
514
|
+
|
515
|
+
if not recommendations:
|
516
|
+
recommendations.append("Cache system is performing well!")
|
517
|
+
|
518
|
+
return recommendations
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""
|
2
|
+
Abstract Base Cache Backend for Django-MOJO Serializers
|
3
|
+
|
4
|
+
Defines the interface that all cache backends must implement to ensure consistency
|
5
|
+
between memory-based, Redis-based, and other caching systems.
|
6
|
+
|
7
|
+
All cache backends must implement this interface for plug-and-play compatibility.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from abc import ABC, abstractmethod
|
11
|
+
from typing import Any, Optional, Dict
|
12
|
+
|
13
|
+
|
14
|
+
class CacheBackend(ABC):
|
15
|
+
"""
|
16
|
+
Abstract base class for serializer cache backends.
|
17
|
+
|
18
|
+
All cache backends must implement this interface to ensure consistency
|
19
|
+
between memory-based and distributed caching systems. This allows for
|
20
|
+
seamless switching between different cache implementations.
|
21
|
+
|
22
|
+
Implementation Requirements:
|
23
|
+
- Thread-safe operations
|
24
|
+
- TTL support (0 = no expiration)
|
25
|
+
- JSON-serializable values only
|
26
|
+
- Graceful error handling
|
27
|
+
- Performance statistics tracking
|
28
|
+
"""
|
29
|
+
|
30
|
+
@abstractmethod
|
31
|
+
def get(self, key: str) -> Optional[Any]:
|
32
|
+
"""
|
33
|
+
Retrieve item from cache.
|
34
|
+
|
35
|
+
Must check TTL expiration and return None for expired items.
|
36
|
+
Should update access patterns for LRU backends.
|
37
|
+
|
38
|
+
:param key: Cache key string
|
39
|
+
:return: Cached value or None if not found/expired
|
40
|
+
"""
|
41
|
+
pass
|
42
|
+
|
43
|
+
@abstractmethod
|
44
|
+
def set(self, key: str, value: Any, ttl: int = 0) -> bool:
|
45
|
+
"""
|
46
|
+
Store item in cache with optional TTL.
|
47
|
+
|
48
|
+
Value must be JSON-serializable. TTL of 0 should mean no caching
|
49
|
+
(safe default behavior).
|
50
|
+
|
51
|
+
:param key: Cache key string
|
52
|
+
:param value: Value to cache (must be JSON serializable)
|
53
|
+
:param ttl: Time-to-live in seconds (0 = no caching/expiration)
|
54
|
+
:return: True if successfully cached, False otherwise
|
55
|
+
"""
|
56
|
+
pass
|
57
|
+
|
58
|
+
@abstractmethod
|
59
|
+
def delete(self, key: str) -> bool:
|
60
|
+
"""
|
61
|
+
Remove specific item from cache.
|
62
|
+
|
63
|
+
:param key: Cache key string
|
64
|
+
:return: True if item was found and removed, False if not found
|
65
|
+
"""
|
66
|
+
pass
|
67
|
+
|
68
|
+
@abstractmethod
|
69
|
+
def clear(self) -> bool:
|
70
|
+
"""
|
71
|
+
Clear all items from cache.
|
72
|
+
|
73
|
+
Should remove all cached items and reset any internal state.
|
74
|
+
|
75
|
+
:return: True if cache was successfully cleared
|
76
|
+
"""
|
77
|
+
pass
|
78
|
+
|
79
|
+
@abstractmethod
|
80
|
+
def stats(self) -> Dict[str, Any]:
|
81
|
+
"""
|
82
|
+
Get cache performance statistics.
|
83
|
+
|
84
|
+
Should return a dictionary with standardized keys for monitoring
|
85
|
+
and debugging purposes. Recommended keys:
|
86
|
+
|
87
|
+
- backend: str (backend type name)
|
88
|
+
- current_size: int (number of cached items)
|
89
|
+
- max_size: int (maximum capacity, 0 if unlimited)
|
90
|
+
- hit_rate: float (0.0-1.0, cache hit percentage)
|
91
|
+
- total_requests: int (hits + misses)
|
92
|
+
- hits: int (cache hits)
|
93
|
+
- misses: int (cache misses)
|
94
|
+
- sets: int (items added to cache)
|
95
|
+
- deletes: int (items removed from cache)
|
96
|
+
- evictions: int (items evicted due to capacity)
|
97
|
+
- expired_items: int (items that expired)
|
98
|
+
- errors: int (operation errors)
|
99
|
+
|
100
|
+
:return: Dictionary with cache statistics
|
101
|
+
"""
|
102
|
+
pass
|