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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /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
@@ -23,4 +23,4 @@ class AuthenticationMiddleware(MiddlewareMixin):
23
23
  if error is not None:
24
24
  return JsonResponse({'error': error}, status=401)
25
25
  request.user = user
26
- user.touch()
26
+ user.track(request)
@@ -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
@@ -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
- # Log the response after get_response has been called
95
+
32
96
  self.log_response(request, response)
33
97
  return response
34
98
 
35
99
  def can_log(self, request):
36
- prefixes = LOGIT_NO_LOG_PREFIX
37
- if not isinstance(prefixes, (list, set, tuple)) or not prefixes:
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
- return not any(request.path.startswith(prefix) for prefix in prefixes)
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
- request.request_log = Log.logit(request, request.DATA.to_json(as_string=True), "request")
161
+ self.queue_log("db", request, request.DATA.to_json(as_string=True), "request")
46
162
  if LOGIT_FILE_ALL:
47
- LOGGER.info(f"REQUEST - {request.method} - {request.ip} - {request.path}", request._raw_body)
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
- Log.logit(request, response.content, "response")
172
+ self.queue_log("db", request, log_content, "response")
54
173
  if LOGIT_FILE_ALL:
55
- LOGGER.info(f"RESPONSE - {request.method} - {request.ip} - {request.path}", response.content)
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: