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,225 @@
|
|
1
|
+
from typing import Optional, List, Set
|
2
|
+
from django.db import models
|
3
|
+
from .client import get_connection
|
4
|
+
|
5
|
+
|
6
|
+
class RedisBasePool:
|
7
|
+
"""Simple Redis pool using atomic Redis operations."""
|
8
|
+
|
9
|
+
def __init__(self, pool_key: str, default_timeout: int = 30):
|
10
|
+
"""
|
11
|
+
Initialize the Redis pool.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
pool_key: Unique identifier for this pool
|
15
|
+
default_timeout: Default timeout in seconds for blocking operations
|
16
|
+
"""
|
17
|
+
self.pool_key = pool_key
|
18
|
+
self.default_timeout = default_timeout
|
19
|
+
self.redis_client = get_connection()
|
20
|
+
|
21
|
+
self.available_list_key = f"{pool_key}:list"
|
22
|
+
self.all_items_set_key = f"{pool_key}:set"
|
23
|
+
|
24
|
+
def add(self, str_id: str) -> bool:
|
25
|
+
"""Add an item to the pool."""
|
26
|
+
if not self.redis_client.sismember(self.all_items_set_key, str_id):
|
27
|
+
self.redis_client.sadd(self.all_items_set_key, str_id)
|
28
|
+
self.redis_client.lpush(self.available_list_key, str_id)
|
29
|
+
return True
|
30
|
+
return False
|
31
|
+
|
32
|
+
def remove(self, str_id: str) -> bool:
|
33
|
+
"""Remove an item from the pool entirely."""
|
34
|
+
if not self.redis_client.sismember(self.all_items_set_key, str_id):
|
35
|
+
return False
|
36
|
+
|
37
|
+
self.redis_client.srem(self.all_items_set_key, str_id)
|
38
|
+
self.redis_client.lrem(self.available_list_key, 0, str_id)
|
39
|
+
return True
|
40
|
+
|
41
|
+
def clear(self) -> None:
|
42
|
+
"""Clear all items from the pool."""
|
43
|
+
self.redis_client.delete(self.available_list_key)
|
44
|
+
self.redis_client.delete(self.all_items_set_key)
|
45
|
+
|
46
|
+
def checkout(self, str_id: str, timeout: Optional[int] = None) -> bool:
|
47
|
+
"""Check out a specific item from the pool."""
|
48
|
+
if not self.redis_client.sismember(self.all_items_set_key, str_id):
|
49
|
+
return False
|
50
|
+
|
51
|
+
if timeout is None:
|
52
|
+
removed = self.redis_client.lrem(self.available_list_key, 1, str_id)
|
53
|
+
return removed > 0
|
54
|
+
|
55
|
+
import time
|
56
|
+
start = time.time()
|
57
|
+
while self.redis_client.lrem(self.available_list_key, 1, str_id) == 0:
|
58
|
+
time.sleep(1.0)
|
59
|
+
elapsed = time.time() - start
|
60
|
+
if elapsed > timeout:
|
61
|
+
return False
|
62
|
+
return True
|
63
|
+
|
64
|
+
def checkin(self, str_id: str) -> bool:
|
65
|
+
"""Check in an item back to the pool."""
|
66
|
+
if not self.redis_client.sismember(self.all_items_set_key, str_id):
|
67
|
+
return False
|
68
|
+
|
69
|
+
self.redis_client.lpush(self.available_list_key, str_id)
|
70
|
+
return True
|
71
|
+
|
72
|
+
def list_all(self) -> Set[str]:
|
73
|
+
"""List all items in the pool."""
|
74
|
+
return self.redis_client.smembers(self.all_items_set_key)
|
75
|
+
|
76
|
+
def list_available(self) -> List[str]:
|
77
|
+
"""List available items in the pool."""
|
78
|
+
return self.redis_client.lrange(self.available_list_key, 0, -1)
|
79
|
+
|
80
|
+
def list_checked_out(self) -> Set[str]:
|
81
|
+
"""List checked out items."""
|
82
|
+
all_items = self.list_all()
|
83
|
+
available_items = set(self.list_available())
|
84
|
+
return all_items - available_items
|
85
|
+
|
86
|
+
def destroy_pool(self) -> None:
|
87
|
+
"""Completely destroy the pool."""
|
88
|
+
self.clear()
|
89
|
+
|
90
|
+
def get_next_available(self, timeout: Optional[int] = None) -> Optional[str]:
|
91
|
+
"""Get the next available item from the pool."""
|
92
|
+
timeout = timeout or self.default_timeout
|
93
|
+
|
94
|
+
result = self.redis_client.brpop(self.available_list_key, timeout=timeout)
|
95
|
+
if result:
|
96
|
+
return result[1]
|
97
|
+
return None
|
98
|
+
|
99
|
+
|
100
|
+
class RedisModelPool(RedisBasePool):
|
101
|
+
"""Django model-specific Redis pool."""
|
102
|
+
|
103
|
+
def __init__(self, model_cls: models.Model, query_dict: dict,
|
104
|
+
pool_key: str, default_timeout: int = 30):
|
105
|
+
"""
|
106
|
+
Initialize the model pool.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
model_cls: Django model class
|
110
|
+
query_dict: Query parameters to filter model instances
|
111
|
+
pool_key: Unique identifier for this pool
|
112
|
+
default_timeout: Default timeout in seconds
|
113
|
+
"""
|
114
|
+
super().__init__(pool_key, default_timeout)
|
115
|
+
self.model_cls = model_cls
|
116
|
+
self.query_dict = query_dict
|
117
|
+
|
118
|
+
def init_pool(self) -> None:
|
119
|
+
"""Initialize pool with model instances."""
|
120
|
+
self.destroy_pool()
|
121
|
+
|
122
|
+
queryset = self.model_cls.objects.filter(**self.query_dict)
|
123
|
+
for instance in queryset:
|
124
|
+
item = str(instance.pk)
|
125
|
+
if not self.redis_client.sismember(self.all_items_set_key, item):
|
126
|
+
self.redis_client.sadd(self.all_items_set_key, item)
|
127
|
+
self.redis_client.lpush(self.available_list_key, item)
|
128
|
+
|
129
|
+
def add_to_pool(self, instance: models.Model) -> bool:
|
130
|
+
"""Add a model instance to the pool."""
|
131
|
+
item = str(instance.pk)
|
132
|
+
if not self.redis_client.sismember(self.all_items_set_key, item):
|
133
|
+
self.redis_client.sadd(self.all_items_set_key, item)
|
134
|
+
self.redis_client.lpush(self.available_list_key, item)
|
135
|
+
return True
|
136
|
+
|
137
|
+
def remove_from_pool(self, instance: models.Model) -> bool:
|
138
|
+
"""Remove instance from pool."""
|
139
|
+
item = str(instance.pk)
|
140
|
+
if not self.redis_client.sismember(self.all_items_set_key, item):
|
141
|
+
return False
|
142
|
+
|
143
|
+
self.redis_client.lrem(self.available_list_key, 0, item)
|
144
|
+
self.redis_client.srem(self.all_items_set_key, item)
|
145
|
+
return True
|
146
|
+
|
147
|
+
def get_next_instance(self, timeout: Optional[int] = None) -> Optional[models.Model]:
|
148
|
+
"""Get the next available model instance."""
|
149
|
+
if not self.redis_client.exists(self.all_items_set_key):
|
150
|
+
self.init_pool()
|
151
|
+
|
152
|
+
pk = self.get_next_available(timeout)
|
153
|
+
if pk:
|
154
|
+
try:
|
155
|
+
instance = self.model_cls.objects.get(pk=pk)
|
156
|
+
|
157
|
+
# Verify instance still matches criteria
|
158
|
+
for key, value in self.query_dict.items():
|
159
|
+
if getattr(instance, key) != value:
|
160
|
+
self.redis_client.srem(self.all_items_set_key, pk)
|
161
|
+
return self.get_next_instance(timeout)
|
162
|
+
|
163
|
+
return instance
|
164
|
+
except self.model_cls.DoesNotExist:
|
165
|
+
self.redis_client.srem(self.all_items_set_key, pk)
|
166
|
+
return self.get_next_instance(timeout)
|
167
|
+
|
168
|
+
return None
|
169
|
+
|
170
|
+
def get_specific_instance(self, instance: models.Model) -> bool:
|
171
|
+
"""Get a specific instance from pool."""
|
172
|
+
item = str(instance.pk)
|
173
|
+
if not self.redis_client.sismember(self.all_items_set_key, item):
|
174
|
+
return False
|
175
|
+
|
176
|
+
removed = self.redis_client.lrem(self.available_list_key, 1, item)
|
177
|
+
return removed > 0
|
178
|
+
|
179
|
+
def return_instance(self, instance: models.Model) -> bool:
|
180
|
+
"""Return a model instance to the pool."""
|
181
|
+
item = str(instance.pk)
|
182
|
+
if not self.redis_client.sismember(self.all_items_set_key, item):
|
183
|
+
return False
|
184
|
+
|
185
|
+
self.redis_client.lpush(self.available_list_key, item)
|
186
|
+
return True
|
187
|
+
|
188
|
+
|
189
|
+
# Example usage:
|
190
|
+
if __name__ == "__main__":
|
191
|
+
# Basic pool
|
192
|
+
pool = RedisBasePool("test_pool")
|
193
|
+
|
194
|
+
# Add items
|
195
|
+
pool.add("item1")
|
196
|
+
pool.add("item2")
|
197
|
+
pool.add("item3")
|
198
|
+
|
199
|
+
print("All items:", pool.list_all())
|
200
|
+
print("Available:", pool.list_available())
|
201
|
+
|
202
|
+
# Get next available
|
203
|
+
item = pool.get_next_available(timeout=5)
|
204
|
+
print(f"Got item: {item}")
|
205
|
+
|
206
|
+
# Return to pool
|
207
|
+
if item:
|
208
|
+
pool.checkin(item)
|
209
|
+
|
210
|
+
# Django model example:
|
211
|
+
# model_pool = RedisModelPool(
|
212
|
+
# model_cls=MyModel,
|
213
|
+
# query_dict={"status": "active"},
|
214
|
+
# pool_key="active_models"
|
215
|
+
# )
|
216
|
+
#
|
217
|
+
# # Initialize pool
|
218
|
+
# model_pool.init_pool()
|
219
|
+
#
|
220
|
+
# # Get instance
|
221
|
+
# instance = model_pool.get_next_instance(timeout=30)
|
222
|
+
# if instance:
|
223
|
+
# print(f"Got instance: {instance}")
|
224
|
+
# # Do work...
|
225
|
+
# model_pool.return_instance(instance)
|
mojo/helpers/request.py
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
from objict import objict, nobjict
|
2
2
|
from .request_parser import RequestDataParser
|
3
|
+
from mojo.helpers.settings import settings
|
4
|
+
|
5
|
+
DUID_HEADER = settings.get('DUID_HEADER', 'X-Mojo-UID').replace('-', '_').upper()
|
6
|
+
DUID_HEADER = f"HTTP_{DUID_HEADER}"
|
3
7
|
|
4
8
|
REQUEST_PARSER = RequestDataParser()
|
5
9
|
|
@@ -43,6 +47,10 @@ def get_remote_ip(request):
|
|
43
47
|
|
44
48
|
def get_device_id(request):
|
45
49
|
# Look for 'buid' or 'duid' in GET parameters
|
50
|
+
duid = request.META.get(DUID_HEADER, None)
|
51
|
+
if duid:
|
52
|
+
return duid
|
53
|
+
|
46
54
|
for key in ['__buid__', 'duid', "buid"]:
|
47
55
|
if key in request.GET:
|
48
56
|
return request.GET[key]
|
mojo/helpers/response.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import
|
1
|
+
from objict import objict
|
2
2
|
from django.http import HttpResponse
|
3
3
|
|
4
4
|
|
@@ -8,7 +8,19 @@ class JsonResponse(HttpResponse):
|
|
8
8
|
raise TypeError(
|
9
9
|
'In order to allow non-dict objects to be serialized set the '
|
10
10
|
'safe parameter to False.'
|
11
|
+
f'Invalid data type: {type(data)}'
|
11
12
|
)
|
12
13
|
kwargs.setdefault('content_type', 'application/json')
|
13
|
-
|
14
|
+
if not isinstance(data, objict):
|
15
|
+
data = objict.from_dict(data)
|
16
|
+
if "code" not in data:
|
17
|
+
data.code = status
|
18
|
+
data = data.to_json(as_string=True)
|
14
19
|
super().__init__(content=data, status=status, **kwargs)
|
20
|
+
|
21
|
+
|
22
|
+
def error(message, status=400):
|
23
|
+
return JsonResponse(objict(error=message), status=status)
|
24
|
+
|
25
|
+
def success(data, status=200):
|
26
|
+
return JsonResponse(objict(status=True, data=data), status=status)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import importlib
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
UNKNOWN = Ellipsis
|
5
5
|
|
@@ -16,42 +16,6 @@ def load_settings_profile(context):
|
|
16
16
|
modules.load_module_to_globals("settings.defaults", context)
|
17
17
|
modules.load_module_to_globals(f"settings.{profile}", context)
|
18
18
|
|
19
|
-
def load_settings_config(context):
|
20
|
-
# Load config from django.conf file
|
21
|
-
from mojo.helpers import paths
|
22
|
-
config_path = paths.VAR_ROOT / "django.conf"
|
23
|
-
if not config_path.exists():
|
24
|
-
raise Exception(f"Required configuration file not found: {config_path}")
|
25
|
-
|
26
|
-
with open(config_path, 'r') as file:
|
27
|
-
for line in file:
|
28
|
-
if '=' in line:
|
29
|
-
key, value = line.strip().split('=', 1)
|
30
|
-
value = value.strip()
|
31
|
-
if value.startswith('"') and value.endswith('"'):
|
32
|
-
value = value[1:-1]
|
33
|
-
elif value.startswith("'") and value.endswith("'"):
|
34
|
-
value = value[1:-1]
|
35
|
-
if value.startswith('f"') or value.startswith("f'"):
|
36
|
-
value = eval(value)
|
37
|
-
elif value.lower() == 'true':
|
38
|
-
value = True
|
39
|
-
elif value.lower() == 'false':
|
40
|
-
value = False
|
41
|
-
else:
|
42
|
-
try:
|
43
|
-
if '.' in value:
|
44
|
-
value = float(value)
|
45
|
-
else:
|
46
|
-
value = int(value)
|
47
|
-
except ValueError:
|
48
|
-
pass
|
49
|
-
context[key.strip()] = value
|
50
|
-
|
51
|
-
if context.get("ALLOW_ADMIN_SITE", True):
|
52
|
-
if "django.contrib.admin" not in context["INSTALLED_APPS"]:
|
53
|
-
context["INSTALLED_APPS"].insert(0, "django.contrib.admin")
|
54
|
-
|
55
19
|
|
56
20
|
class SettingsHelper:
|
57
21
|
"""
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class DjangoConfigLoader:
|
2
|
+
"""
|
3
|
+
A clean, expandable class for loading Django configuration from django.conf files.
|
4
|
+
"""
|
5
|
+
|
6
|
+
def __init__(self, config_path=None):
|
7
|
+
"""
|
8
|
+
Initialize the config loader.
|
9
|
+
|
10
|
+
:param config_path: Path to the django.conf file. If None, uses default VAR_ROOT path.
|
11
|
+
"""
|
12
|
+
if config_path is None:
|
13
|
+
from mojo.helpers import paths
|
14
|
+
self.config_path = paths.VAR_ROOT / "django.conf"
|
15
|
+
else:
|
16
|
+
self.config_path = config_path
|
17
|
+
|
18
|
+
def load_config(self, context):
|
19
|
+
"""
|
20
|
+
Load configuration from django.conf file into the provided context.
|
21
|
+
|
22
|
+
:param context: Dictionary to load configuration values into.
|
23
|
+
:raises Exception: If the required configuration file is not found.
|
24
|
+
"""
|
25
|
+
self._validate_config_file()
|
26
|
+
self._parse_config_file(context)
|
27
|
+
self._apply_admin_site_config(context)
|
28
|
+
|
29
|
+
def _validate_config_file(self):
|
30
|
+
"""Validate that the configuration file exists."""
|
31
|
+
if not self.config_path.exists():
|
32
|
+
raise Exception(f"Required configuration file not found: {self.config_path}")
|
33
|
+
|
34
|
+
def _parse_config_file(self, context):
|
35
|
+
"""Parse the configuration file and populate the context."""
|
36
|
+
with open(self.config_path, 'r') as file:
|
37
|
+
for line in file:
|
38
|
+
if '=' in line:
|
39
|
+
key, value = line.strip().split('=', 1)
|
40
|
+
parsed_value = self._parse_value(value.strip())
|
41
|
+
context[key.strip()] = parsed_value
|
42
|
+
|
43
|
+
def _parse_value(self, value):
|
44
|
+
"""
|
45
|
+
Parse a configuration value string into the appropriate Python type.
|
46
|
+
|
47
|
+
:param value: String value to parse.
|
48
|
+
:return: Parsed value with appropriate type.
|
49
|
+
"""
|
50
|
+
if self._is_list_value(value):
|
51
|
+
return self._parse_list_value(value)
|
52
|
+
elif self._is_quoted_string(value):
|
53
|
+
return self._parse_quoted_string(value)
|
54
|
+
elif self._is_f_string(value):
|
55
|
+
return eval(value)
|
56
|
+
elif self._is_boolean(value):
|
57
|
+
return self._parse_boolean(value)
|
58
|
+
else:
|
59
|
+
return self._parse_numeric_or_string(value)
|
60
|
+
|
61
|
+
def _is_list_value(self, value):
|
62
|
+
"""Check if value is a list format."""
|
63
|
+
return value.startswith('[') and value.endswith(']')
|
64
|
+
|
65
|
+
def _is_quoted_string(self, value):
|
66
|
+
"""Check if value is a quoted string."""
|
67
|
+
return ((value.startswith('"') and value.endswith('"')) or
|
68
|
+
(value.startswith("'") and value.endswith("'")))
|
69
|
+
|
70
|
+
def _is_f_string(self, value):
|
71
|
+
"""Check if value is an f-string."""
|
72
|
+
return value.startswith('f"') or value.startswith("f'")
|
73
|
+
|
74
|
+
def _is_boolean(self, value):
|
75
|
+
"""Check if value is a boolean."""
|
76
|
+
return value.lower() in ('true', 'false')
|
77
|
+
|
78
|
+
def _parse_list_value(self, value):
|
79
|
+
"""Parse a list value string into a Python list."""
|
80
|
+
list_content = value[1:-1].strip()
|
81
|
+
if not list_content:
|
82
|
+
return []
|
83
|
+
|
84
|
+
items = []
|
85
|
+
for item in list_content.split(','):
|
86
|
+
item = item.strip()
|
87
|
+
parsed_item = self._parse_list_item(item)
|
88
|
+
items.append(parsed_item)
|
89
|
+
return items
|
90
|
+
|
91
|
+
def _parse_list_item(self, item):
|
92
|
+
"""Parse an individual list item."""
|
93
|
+
if self._is_quoted_string(item):
|
94
|
+
return item[1:-1] # Remove quotes
|
95
|
+
else:
|
96
|
+
return self._parse_numeric_or_string(item)
|
97
|
+
|
98
|
+
def _parse_quoted_string(self, value):
|
99
|
+
"""Parse a quoted string by removing the quotes."""
|
100
|
+
return value[1:-1]
|
101
|
+
|
102
|
+
def _parse_boolean(self, value):
|
103
|
+
"""Parse a boolean string."""
|
104
|
+
return value.lower() == 'true'
|
105
|
+
|
106
|
+
def _parse_numeric_or_string(self, value):
|
107
|
+
"""Parse a value as numeric if possible, otherwise return as string."""
|
108
|
+
try:
|
109
|
+
if '.' in value:
|
110
|
+
return float(value)
|
111
|
+
else:
|
112
|
+
return int(value)
|
113
|
+
except ValueError:
|
114
|
+
return value
|
115
|
+
|
116
|
+
def _apply_admin_site_config(self, context):
|
117
|
+
"""Apply Django admin site configuration if enabled."""
|
118
|
+
if context.get("ALLOW_ADMIN_SITE", True):
|
119
|
+
installed_apps = context.get("INSTALLED_APPS", [])
|
120
|
+
if "django.contrib.admin" not in installed_apps:
|
121
|
+
installed_apps.insert(0, "django.contrib.admin")
|
122
|
+
context["INSTALLED_APPS"] = installed_apps
|
123
|
+
|
124
|
+
|
125
|
+
def load_settings_config(context):
|
126
|
+
"""
|
127
|
+
Load Django configuration from django.conf file.
|
128
|
+
|
129
|
+
:param context: Dictionary to load configuration values into.
|
130
|
+
"""
|
131
|
+
loader = DjangoConfigLoader()
|
132
|
+
loader.load_config(context)
|
mojo/middleware/auth.py
CHANGED
mojo/middleware/cors.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from django.http import HttpResponse
|
2
|
+
from mojo.helpers.settings import settings
|
3
|
+
|
4
|
+
DUID_HEADER = settings.get('DUID_HEADER', 'X-Mojo-UID')
|
5
|
+
|
6
|
+
# middleware/cors.py
|
7
|
+
class CORSMiddleware:
|
8
|
+
def __init__(self, get_response):
|
9
|
+
self.get_response = get_response
|
10
|
+
|
11
|
+
def __call__(self, request):
|
12
|
+
# Handle preflight requests
|
13
|
+
if request.method == 'OPTIONS':
|
14
|
+
response = HttpResponse()
|
15
|
+
else:
|
16
|
+
response = self.get_response(request)
|
17
|
+
|
18
|
+
# Always allow all origins
|
19
|
+
response['Access-Control-Allow-Origin'] = '*'
|
20
|
+
|
21
|
+
# Allow credentials if needed (note: can't use * origin with credentials)
|
22
|
+
# response['Access-Control-Allow-Credentials'] = 'true'
|
23
|
+
|
24
|
+
# Allow all methods to minimize preflight requests
|
25
|
+
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS'
|
26
|
+
|
27
|
+
# Allow common headers to minimize preflight requests
|
28
|
+
response['Access-Control-Allow-Headers'] = (
|
29
|
+
'Accept, Accept-Encoding, Authorization, Content-Type, '
|
30
|
+
'Origin, User-Agent, X-Requested-With, X-CSRFToken, '
|
31
|
+
f'X-API-Key, {DUID_HEADER}, Cache-Control, Pragma'
|
32
|
+
)
|
33
|
+
|
34
|
+
# Long preflight cache (24 hours)
|
35
|
+
response['Access-Control-Max-Age'] = '86400'
|
36
|
+
|
37
|
+
# Expose headers that frontend might need
|
38
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition, X-Total-Count'
|
39
|
+
|
40
|
+
return response
|