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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /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 ujson
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
- data = ujson.dumps(data)
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)
@@ -0,0 +1,2 @@
1
+ from .helper import load_settings_profile, settings
2
+ from .parser import load_settings_config
@@ -1,5 +1,5 @@
1
1
  import importlib
2
- from typing import Any, Union
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
@@ -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