django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.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 +279 -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.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.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,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
@@ -8,6 +8,7 @@ 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):
|
@@ -16,3 +17,10 @@ class JsonResponse(HttpResponse):
|
|
16
17
|
data.code = status
|
17
18
|
data = data.to_json(as_string=True)
|
18
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)
|
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
|
mojo/middleware/logging.py
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
import json
|
2
|
+
import threading
|
3
|
+
from queue import Queue, Empty
|
1
4
|
from mojo.apps.logit.models import Log
|
2
5
|
from mojo.helpers.settings import settings
|
3
6
|
from mojo.helpers import logit
|
@@ -7,49 +10,165 @@ API_PREFIX = "/".join([settings.get("MOJO_PREFIX", "api/").rstrip("/"), ""])
|
|
7
10
|
LOGIT_DB_ALL = settings.get("LOGIT_DB_ALL", False)
|
8
11
|
LOGIT_FILE_ALL = settings.get("LOGIT_FILE_ALL", False)
|
9
12
|
LOGIT_RETURN_REAL_ERROR = settings.get("LOGIT_RETURN_REAL_ERROR", True)
|
13
|
+
LOGIT_MAX_RESPONSE_SIZE = settings.get("LOGIT_MAX_RESPONSE_SIZE", 1024) # 1KB default
|
10
14
|
LOGGER = logit.get_logger("requests", "requests.log")
|
11
15
|
ERROR_LOGGER = logit.get_logger("error", "error.log")
|
12
|
-
LOGIT_NO_LOG_PREFIX = settings.get("LOGIT_NO_LOG_PREFIX", [])
|
16
|
+
LOGIT_NO_LOG_PREFIX = settings.get("LOGIT_NO_LOG_PREFIX", ['GET:/api/user'])
|
17
|
+
LOGIT_ALWAYS_LOG_PREFIX = settings.get("LOGIT_ALWAYS_LOG_PREFIX", ['POST:/api/user', 'GET:/api/user/'])
|
18
|
+
|
19
|
+
# Async logging setup
|
20
|
+
log_queue = Queue()
|
21
|
+
background_thread = None
|
22
|
+
|
23
|
+
def background_logger():
|
24
|
+
"""Background thread to process logs without blocking responses."""
|
25
|
+
while True:
|
26
|
+
try:
|
27
|
+
log_item = log_queue.get(timeout=30) # 30s timeout
|
28
|
+
if log_item is None: # Shutdown signal
|
29
|
+
break
|
30
|
+
|
31
|
+
log_type, request, content, log_kind = log_item
|
32
|
+
|
33
|
+
if log_type == "db":
|
34
|
+
Log.logit(request, content, log_kind)
|
35
|
+
elif log_type == "file":
|
36
|
+
method = request.method if request else "SYSTEM"
|
37
|
+
ip = getattr(request, 'ip', 'unknown') if request else 'system'
|
38
|
+
path = getattr(request, 'path', 'unknown') if request else 'system'
|
39
|
+
LOGGER.info(f"{log_kind.upper()} - {method} - {ip} - {path}", content)
|
40
|
+
|
41
|
+
log_queue.task_done()
|
42
|
+
except Empty:
|
43
|
+
continue
|
44
|
+
except Exception as e:
|
45
|
+
ERROR_LOGGER.exception(f"Background logging error: {e}")
|
46
|
+
|
47
|
+
def start_background_logger():
|
48
|
+
global background_thread
|
49
|
+
if background_thread is None or not background_thread.is_alive():
|
50
|
+
background_thread = threading.Thread(target=background_logger, daemon=True)
|
51
|
+
background_thread.start()
|
52
|
+
|
53
|
+
# Start the background logger
|
54
|
+
start_background_logger()
|
13
55
|
|
14
56
|
class LoggerMiddleware:
|
15
57
|
def __init__(self, get_response):
|
16
58
|
self.get_response = get_response
|
17
59
|
|
60
|
+
def _request_matches_prefix(self, request, prefix):
|
61
|
+
"""
|
62
|
+
Checks if a request matches a given prefix rule.
|
63
|
+
The rule can be a simple path prefix or a "METHOD:/path/prefix".
|
64
|
+
"""
|
65
|
+
method = None
|
66
|
+
path_prefix = prefix
|
67
|
+
|
68
|
+
if ":" in prefix:
|
69
|
+
parts = prefix.split(":", 1)
|
70
|
+
# Ensure it's a valid METHOD:/path format
|
71
|
+
if len(parts) == 2 and parts[0].upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'):
|
72
|
+
method, path_prefix = parts[0].upper(), parts[1]
|
73
|
+
|
74
|
+
# Check if the path matches
|
75
|
+
if not request.path.startswith(path_prefix):
|
76
|
+
return False
|
77
|
+
|
78
|
+
# If a method was specified in the rule, check if it matches
|
79
|
+
if method and request.method != method:
|
80
|
+
return False
|
81
|
+
|
82
|
+
return True
|
83
|
+
|
18
84
|
def __call__(self, request):
|
19
|
-
# Log the request before calling get_response
|
20
85
|
self.log_request(request)
|
21
86
|
try:
|
22
87
|
response = self.get_response(request)
|
23
88
|
except Exception as e:
|
24
|
-
# Log or store the exception here
|
25
89
|
err = ERROR_LOGGER.exception()
|
26
|
-
Log.logit(request, err, "api_error")
|
90
|
+
Log.logit(request, err, "api_error") # Keep errors synchronous
|
27
91
|
error = "system error"
|
28
92
|
if LOGIT_RETURN_REAL_ERROR:
|
29
93
|
error = str(e)
|
30
94
|
response = JsonResponse(dict(status=False, error=error), status=500)
|
31
|
-
|
95
|
+
|
32
96
|
self.log_response(request, response)
|
33
97
|
return response
|
34
98
|
|
35
99
|
def can_log(self, request):
|
36
|
-
|
37
|
-
if
|
100
|
+
"""
|
101
|
+
Determines if a request should be logged based on settings.
|
102
|
+
- LOGIT_ALWAYS_LOG_PREFIX overrides LOGIT_NO_LOG_PREFIX.
|
103
|
+
- Rules can be "METHOD:/path/prefix" or "/path/prefix".
|
104
|
+
"""
|
105
|
+
# 1. Check LOGIT_ALWAYS_LOG_PREFIX first. If it matches, we must log.
|
106
|
+
for prefix in LOGIT_ALWAYS_LOG_PREFIX:
|
107
|
+
if self._request_matches_prefix(request, prefix):
|
108
|
+
return True
|
109
|
+
|
110
|
+
# 2. Check LOGIT_NO_LOG_PREFIX. If it matches, we must NOT log.
|
111
|
+
for prefix in LOGIT_NO_LOG_PREFIX:
|
112
|
+
if self._request_matches_prefix(request, prefix):
|
113
|
+
return False
|
114
|
+
|
115
|
+
# 3. Default behavior: log if no specific rules apply.
|
116
|
+
return True
|
117
|
+
|
118
|
+
def should_log_full_content(self, request, response):
|
119
|
+
"""Fast conditional checks to decide logging strategy."""
|
120
|
+
# Always log errors fully (but still async)
|
121
|
+
if response.status_code >= 400:
|
38
122
|
return True
|
39
|
-
|
123
|
+
|
124
|
+
# Quick size check
|
125
|
+
content_length = len(response.content)
|
126
|
+
if content_length > LOGIT_MAX_RESPONSE_SIZE:
|
127
|
+
return False
|
128
|
+
|
129
|
+
# Path-based decisions
|
130
|
+
if request.path.endswith('/list/') or '/list?' in request.path:
|
131
|
+
return False
|
132
|
+
|
133
|
+
return True
|
134
|
+
|
135
|
+
def get_response_log_content(self, request, response):
|
136
|
+
"""Extract log content - prioritize log_context if available."""
|
137
|
+
|
138
|
+
# Check for log_context first (fastest path)
|
139
|
+
if hasattr(response, 'log_context') and response.log_context:
|
140
|
+
return json.dumps(response.log_context)
|
141
|
+
|
142
|
+
# Conditional processing based on fast checks
|
143
|
+
if not self.should_log_full_content(request, response):
|
144
|
+
return f"Response: {response.status_code}, Size: {len(response.content)} bytes"
|
145
|
+
|
146
|
+
# For small responses, log full content
|
147
|
+
return response.content
|
148
|
+
|
149
|
+
def queue_log(self, log_type, request, content, log_kind):
|
150
|
+
"""Queue log for background processing."""
|
151
|
+
try:
|
152
|
+
log_queue.put((log_type, request, content, log_kind), block=False)
|
153
|
+
except:
|
154
|
+
# If queue is full, just skip this log to avoid blocking
|
155
|
+
pass
|
40
156
|
|
41
157
|
def log_request(self, request):
|
42
158
|
if not self.can_log(request):
|
43
159
|
return
|
44
160
|
if LOGIT_DB_ALL:
|
45
|
-
|
161
|
+
self.queue_log("db", request, request.DATA.to_json(as_string=True), "request")
|
46
162
|
if LOGIT_FILE_ALL:
|
47
|
-
|
163
|
+
self.queue_log("file", request, request._raw_body, "request")
|
48
164
|
|
49
165
|
def log_response(self, request, response):
|
50
166
|
if not self.can_log(request):
|
51
167
|
return
|
168
|
+
|
169
|
+
log_content = self.get_response_log_content(request, response)
|
170
|
+
|
52
171
|
if LOGIT_DB_ALL:
|
53
|
-
|
172
|
+
self.queue_log("db", request, log_content, "response")
|
54
173
|
if LOGIT_FILE_ALL:
|
55
|
-
|
174
|
+
self.queue_log("file", request, log_content, "response")
|
mojo/middleware/mojo.py
CHANGED
@@ -2,6 +2,10 @@ from mojo.helpers import request as rhelper
|
|
2
2
|
import time
|
3
3
|
from objict import objict
|
4
4
|
from mojo.helpers.settings import settings
|
5
|
+
from mojo.helpers import dates, logit
|
6
|
+
|
7
|
+
|
8
|
+
logger = logit.get_logger("debug", "debug.log")
|
5
9
|
|
6
10
|
ANONYMOUS_USER = objict(is_authenticated=False)
|
7
11
|
|
@@ -18,6 +22,7 @@ class MojoMiddleware:
|
|
18
22
|
request.ip = rhelper.get_remote_ip(request)
|
19
23
|
request.user_agent = rhelper.get_user_agent(request)
|
20
24
|
request.duid = rhelper.get_device_id(request)
|
25
|
+
# logger.info(f"duid: {request.duid}", request.META)
|
21
26
|
if settings.LOGIT_REQUEST_BODY:
|
22
27
|
request._raw_body = str(request.body)
|
23
28
|
else:
|