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,99 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps import metrics
3
+ from mojo.helpers.response import JsonResponse
4
+ import mojo.errors
5
+
6
+
7
+ @md.URL('permissions')
8
+ @md.URL('permissions/<str:account>')
9
+ @md.requires_perms("manage_incidents")
10
+ def on_permissions(request, account=None):
11
+ if request.method == 'GET':
12
+ if account is None:
13
+ return on_list_permissions(request)
14
+ return on_get_permissions(request, account)
15
+ if request.method == 'POST':
16
+ if not account:
17
+ account = request.DATA.get("account", None)
18
+ if account:
19
+ return on_set_permissions(request, account)
20
+ if request.method == 'DELETE' and account:
21
+ return on_delete_permissions(request, account)
22
+ return JsonResponse({
23
+ "method": request.method,
24
+ "error": "Invalid method",
25
+ "status": False
26
+ })
27
+
28
+ def on_get_permissions(request, account):
29
+ """
30
+ Get current view and write permissions for an account.
31
+ """
32
+ view_perms = metrics.get_view_perms(account)
33
+ write_perms = metrics.get_write_perms(account)
34
+
35
+ return JsonResponse({
36
+ "id": account,
37
+ "account": account,
38
+ "view_permissions": view_perms,
39
+ "write_permissions": write_perms,
40
+ "status": True
41
+ })
42
+
43
+
44
+ def on_set_permissions(request, account):
45
+ """
46
+ Set view permissions for an account.
47
+ """
48
+ view_perms = request.DATA.get("view_permissions", "").split(",")
49
+ write_perms = request.DATA.get("write_permissions", "").split(",")
50
+
51
+ if view_perms:
52
+ metrics.set_view_perms(account, view_perms)
53
+ if write_perms:
54
+ metrics.set_write_perms(account, write_perms)
55
+
56
+ return JsonResponse({
57
+ "id": account,
58
+ "account": account,
59
+ "view_permissions": view_perms,
60
+ "write_permissions": write_perms,
61
+ "action": "set",
62
+ "status": True
63
+ })
64
+
65
+
66
+ def on_delete_permissions(request, account):
67
+ """
68
+ Remove all permissions for an account.
69
+ """
70
+ # Remove both view and write permissions
71
+ metrics.set_view_perms(account, None)
72
+ metrics.set_write_perms(account, None)
73
+
74
+ return JsonResponse({
75
+ "account": account,
76
+ "action": "deleted",
77
+ "status": True
78
+ })
79
+
80
+
81
+ def on_list_permissions(request):
82
+ """
83
+ List all accounts that have permissions configured.
84
+ """
85
+ accounts = metrics.list_accounts()
86
+ data = []
87
+ for account in accounts:
88
+ info = {"account": account, "id": account}
89
+ info["view_permissions"] = metrics.get_view_perms(account)
90
+ info["write_permissions"] = metrics.get_write_perms(account)
91
+ data.append(info)
92
+
93
+ return JsonResponse({
94
+ "data": data,
95
+ "size": 10,
96
+ "start": 0,
97
+ "count": len(accounts),
98
+ "status": True
99
+ })
@@ -0,0 +1,277 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps import metrics
3
+ from mojo.helpers.response import JsonResponse
4
+ import mojo.errors
5
+ import datetime
6
+ from .helpers import check_view_permissions, check_write_permissions
7
+
8
+ # Documentation for time-series metrics endpoints
9
+ SERIES_GET_DOCS = {
10
+ "summary": "Get time-series values at a point in time",
11
+ "description": "Retrieves time-series metric values for multiple slugs at a specific datetime and granularity.",
12
+ "parameters": [
13
+ {
14
+ "name": "slugs",
15
+ "in": "query",
16
+ "required": True,
17
+ "schema": {"type": "string"},
18
+ "description": "Comma-separated list of slugs to fetch values for (e.g., 'user_activity_day,page_views')."
19
+ },
20
+ {
21
+ "name": "when",
22
+ "in": "query",
23
+ "required": True,
24
+ "schema": {"type": "string", "format": "date-time"},
25
+ "description": "The specific date/datetime to fetch values for."
26
+ },
27
+ {
28
+ "name": "granularity",
29
+ "in": "query",
30
+ "schema": {"type": "string", "default": "hours"},
31
+ "description": "Granularity of the data (e.g., 'hours', 'days', 'months', 'years')."
32
+ },
33
+ {
34
+ "name": "account",
35
+ "in": "query",
36
+ "schema": {"type": "string", "default": "public"},
37
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
38
+ }
39
+ ],
40
+ "responses": {
41
+ "200": {
42
+ "description": "Successful response with metric values.",
43
+ "content": {
44
+ "application/json": {
45
+ "example": {
46
+ "response": {
47
+ "data": {
48
+ "user_activity_day": 22,
49
+ "page_views": 156
50
+ },
51
+ "slugs": ["user_activity_day", "page_views"],
52
+ "when": "2025-08-01T00:00:00",
53
+ "granularity": "days",
54
+ "account": "public",
55
+ "status": True
56
+ },
57
+ "status_code": 200
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ SERIES_POST_DOCS = {
66
+ "summary": "Get time-series values at a point in time (POST)",
67
+ "description": "Retrieves time-series metric values for multiple slugs at a specific datetime and granularity using POST method.",
68
+ "parameters": [
69
+ {
70
+ "name": "slugs",
71
+ "in": "body",
72
+ "required": True,
73
+ "schema": {"type": "string"},
74
+ "description": "Comma-separated list of slugs to fetch values for (e.g., 'user_activity_day,page_views')."
75
+ },
76
+ {
77
+ "name": "when",
78
+ "in": "body",
79
+ "required": True,
80
+ "schema": {"type": "string", "format": "date-time"},
81
+ "description": "The specific date/datetime to fetch values for."
82
+ },
83
+ {
84
+ "name": "granularity",
85
+ "in": "body",
86
+ "schema": {"type": "string", "default": "hours"},
87
+ "description": "Granularity of the data (e.g., 'hours', 'days', 'months', 'years')."
88
+ },
89
+ {
90
+ "name": "account",
91
+ "in": "body",
92
+ "schema": {"type": "string", "default": "public"},
93
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
94
+ }
95
+ ]
96
+ }
97
+
98
+ # Documentation for simple value storage endpoints
99
+ SET_VALUE_DOCS = {
100
+ "summary": "Set simple global values",
101
+ "description": "Sets simple key-value pairs for global storage (not time-series).",
102
+ "parameters": [
103
+ {
104
+ "name": "slug",
105
+ "in": "body",
106
+ "required": True,
107
+ "schema": {"type": "string"},
108
+ "description": "The key identifier for the value to set."
109
+ },
110
+ {
111
+ "name": "value",
112
+ "in": "body",
113
+ "required": True,
114
+ "schema": {"type": "string"},
115
+ "description": "The value to store."
116
+ },
117
+ {
118
+ "name": "account",
119
+ "in": "body",
120
+ "schema": {"type": "string", "default": "public"},
121
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
122
+ }
123
+ ],
124
+ "responses": {
125
+ "200": {
126
+ "description": "Successful value storage.",
127
+ "content": {
128
+ "application/json": {
129
+ "example": {
130
+ "response": {
131
+ "slug": "max_users",
132
+ "value": "1000",
133
+ "account": "public",
134
+ "status": True
135
+ },
136
+ "status_code": 200
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ VALUE_GET_DOCS = {
145
+ "summary": "Get simple global values",
146
+ "description": "Retrieves simple key-value pairs from global storage (not time-series). Supports multiple slugs.",
147
+ "parameters": [
148
+ {
149
+ "name": "slugs",
150
+ "in": "query",
151
+ "required": True,
152
+ "schema": {"type": "string"},
153
+ "description": "Comma-separated list of slugs to fetch values for (e.g., 'max_users,maintenance_mode')."
154
+ },
155
+ {
156
+ "name": "account",
157
+ "in": "query",
158
+ "schema": {"type": "string", "default": "public"},
159
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
160
+ },
161
+ {
162
+ "name": "default",
163
+ "in": "query",
164
+ "schema": {"type": "string"},
165
+ "description": "Default value to return for missing keys."
166
+ }
167
+ ],
168
+ "responses": {
169
+ "200": {
170
+ "description": "Successful response with values.",
171
+ "content": {
172
+ "application/json": {
173
+ "example": {
174
+ "response": {
175
+ "data": {
176
+ "max_users": "1000",
177
+ "maintenance_mode": "false"
178
+ },
179
+ "slugs": ["max_users", "maintenance_mode"],
180
+ "account": "public",
181
+ "status": True
182
+ },
183
+ "status_code": 200
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+
192
+ # Time-series metrics endpoints
193
+ @md.GET('series', docs=SERIES_GET_DOCS)
194
+ @md.requires_params("slugs")
195
+ def on_metrics_series(request):
196
+ """
197
+ Get time-series values for multiple slugs at a single point in time.
198
+ """
199
+ when = request.DATA.get_typed("when")
200
+ account = request.DATA.get("account", "public")
201
+ granularity = request.DATA.get("granularity", "hours")
202
+ slugs = request.DATA.get("slugs")
203
+
204
+ check_view_permissions(request, account)
205
+
206
+ # Fetch the values using our new method
207
+ result = metrics.fetch_values(slugs, when, granularity, account=account)
208
+ result['status'] = True
209
+
210
+ return JsonResponse(result)
211
+
212
+
213
+ @md.POST('series', docs=SERIES_POST_DOCS)
214
+ @md.requires_params("slugs")
215
+ def on_metrics_series_post(request):
216
+ """
217
+ POST version of the series endpoint - same functionality as GET.
218
+ """
219
+ return on_metrics_series(request)
220
+
221
+
222
+ # Simple value storage endpoints
223
+ @md.POST('value/set', docs=SET_VALUE_DOCS)
224
+ @md.requires_params("slug", "value")
225
+ def on_set_value(request):
226
+ """
227
+ Set a simple global value (not time-series).
228
+ """
229
+ slug = request.DATA.get("slug")
230
+ value = request.DATA.get("value")
231
+ account = request.DATA.get("account", "public")
232
+
233
+ check_write_permissions(request, account)
234
+
235
+ metrics.set_value(slug, value, account=account)
236
+
237
+ return JsonResponse({
238
+ "slug": slug,
239
+ "value": value,
240
+ "account": account,
241
+ "status": True
242
+ })
243
+
244
+
245
+ @md.GET('value/get', docs=VALUE_GET_DOCS)
246
+ @md.requires_params("slugs")
247
+ def on_get_value(request):
248
+ """
249
+ Get simple global values for multiple slugs (not time-series).
250
+ """
251
+ slugs = request.DATA.get("slugs")
252
+ account = request.DATA.get("account", "public")
253
+ default = request.DATA.get("default")
254
+
255
+ check_view_permissions(request, account)
256
+
257
+ # Handle comma-separated string input
258
+ if isinstance(slugs, str):
259
+ if ',' in slugs:
260
+ slugs_list = [s.strip() for s in slugs.split(',')]
261
+ else:
262
+ slugs_list = [slugs]
263
+ else:
264
+ slugs_list = slugs
265
+
266
+ # Fetch values for all slugs
267
+ data = {}
268
+ for slug in slugs_list:
269
+ value = metrics.get_value(slug, account=account, default=default)
270
+ data[slug] = value
271
+
272
+ return JsonResponse({
273
+ "data": data,
274
+ "slugs": slugs_list,
275
+ "account": account,
276
+ "status": True
277
+ })
@@ -34,8 +34,8 @@ GRANULARITY_OFFSET_MAP = {
34
34
 
35
35
  GRANULARITY_END_MAP = {
36
36
  'minutes': timedelta(minutes=29),
37
- 'hours': timedelta(hours=11),
38
- 'days': timedelta(days=11),
37
+ 'hours': timedelta(hours=24),
38
+ 'days': timedelta(days=30),
39
39
  'weeks': timedelta(weeks=11),
40
40
  'months': timedelta(days=12*30),
41
41
  'years': timedelta(days=11*360)
@@ -179,6 +179,23 @@ def generate_perm_write_key(account):
179
179
  def generate_perm_view_key(account):
180
180
  return f"mets:{account}:perm:v"
181
181
 
182
+ def generate_accounts_key():
183
+ return f"mets:_accounts_"
184
+
185
+ def generate_value_key(slug, account):
186
+ """
187
+ Generate a Redis key for storing simple key-value pairs.
188
+
189
+ Args:
190
+ slug (str): The slug identifier for the value.
191
+ account (str): The account under which the value is stored.
192
+
193
+ Returns:
194
+ str: The Redis key for the value.
195
+ """
196
+ normalized_slug = normalize_slug(slug)
197
+ return f"mets:{account}:val:{normalized_slug}"
198
+
182
199
 
183
200
  def normalize_slug(slug):
184
201
  return slug.replace(':', '|')
mojo/decorators/auth.py CHANGED
@@ -1,5 +1,8 @@
1
1
  from functools import wraps
2
2
  import mojo.errors
3
+ from mojo.helpers import logit
4
+
5
+ logger = logit.get_logger("error", "error.log")
3
6
 
4
7
  def requires_perms(*required_perms):
5
8
  def decorator(func):
@@ -7,7 +10,9 @@ def requires_perms(*required_perms):
7
10
  def wrapper(request, *args, **kwargs):
8
11
  if not request.user.is_authenticated:
9
12
  raise mojo.errors.PermissionDeniedException()
10
- if not request.user.has_permission(required_perms):
13
+ perms = set(required_perms)
14
+ if not request.user.has_permission(perms):
15
+ logger.error(f"{request.user.username} is missing {perms}")
11
16
  raise mojo.errors.PermissionDeniedException()
12
17
  return func(request, *args, **kwargs)
13
18
  return wrapper
mojo/decorators/http.py CHANGED
@@ -10,6 +10,7 @@ from mojo.helpers.response import JsonResponse
10
10
  from functools import wraps
11
11
  from mojo.helpers import modules
12
12
  from mojo.models import rest
13
+ from django.http import HttpResponse
13
14
  from mojo.apps import metrics
14
15
 
15
16
  logger = logit.get_logger("error", "error.log")
@@ -23,6 +24,7 @@ MOJO_APPEND_SLASH = settings.get("MOJO_APPEND_SLASH", False)
23
24
 
24
25
  API_METRICS = settings.get("API_METRICS", False)
25
26
  API_METRICS_GRANULARITY = settings.get("API_METRICS_GRANULARITY", "days")
27
+ EVENTS_ON_ERRORS = settings.get("EVENTS_ON_ERRORS", True)
26
28
 
27
29
 
28
30
  def dispatcher(request, *args, **kwargs):
@@ -32,7 +34,19 @@ def dispatcher(request, *args, **kwargs):
32
34
  rest.ACTIVE_REQUEST = request
33
35
  key = kwargs.pop('__mojo_rest_root_key__', None)
34
36
  if "group" in request.DATA:
35
- request.group = modules.get_model_instance("account", "Group", int(request.DATA.group))
37
+ try:
38
+ request.group = modules.get_model_instance("account", "Group", int(request.DATA.group))
39
+ if request.group is not None:
40
+ request.group.touch()
41
+ except ValueError:
42
+ if EVENTS_ON_ERRORS:
43
+ rest.MojoModel.class_report_incident(
44
+ details=f"Permission denied: Invalid group ID -> '{request.DATA.group}'",
45
+ event_type="rest_error",
46
+ request=request,
47
+ request_path=getattr(request, "path", None),
48
+ )
49
+ return JsonResponse({"error": "Invalid group ID", "code": 400}, status=400)
36
50
  method_key = f"{key}__{request.method}"
37
51
  if method_key not in URLPATTERN_METHODS:
38
52
  method_key = f"{key}__ALL"
@@ -51,22 +65,52 @@ def dispatch_error_handler(func):
51
65
  try:
52
66
  if API_METRICS:
53
67
  metrics.record("api_calls", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
54
- return func(request, *args, **kwargs)
68
+ resp = func(request, *args, **kwargs)
69
+ if not isinstance(resp, HttpResponse) and isinstance(resp, dict):
70
+ return JsonResponse(resp)
71
+ return resp
55
72
  except mojo.errors.MojoException as err:
56
73
  if API_METRICS:
57
74
  metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
75
+ if EVENTS_ON_ERRORS:
76
+ rest.MojoModel.class_report_incident_for_user(
77
+ details=f"Rest Mojo Error: {err.reason}",
78
+ event_type="rest_error",
79
+ request_data=request.DATA,
80
+ request=request,
81
+ request_path=getattr(request, "path", None),
82
+ stack_trace=traceback.format_exc(),
83
+ )
58
84
  return JsonResponse({"error": err.reason, "code": err.code}, status=err.status)
59
85
  except ValueError as err:
60
86
  if API_METRICS:
61
87
  metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
62
88
  logger.exception(f"ValueErrror: {str(err)}, Path: {request.path}, IP: {request.META.get('REMOTE_ADDR')}")
89
+ if EVENTS_ON_ERRORS:
90
+ rest.MojoModel.class_report_incident_for_user(
91
+ details=f"Rest Value Error: {err}",
92
+ event_type="rest_error",
93
+ request_data=request.DATA,
94
+ request=request,
95
+ request_path=getattr(request, "path", None),
96
+ stack_trace=traceback.format_exc()
97
+ )
63
98
  return JsonResponse({"error": str(err), "code": 555 }, status=500)
64
99
  except Exception as err:
65
100
  if API_METRICS:
66
101
  metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
67
102
  # logger.exception(f"Unhandled REST Exception: {request.path}")
68
103
  logger.exception(f"Error: {str(err)}, Path: {request.path}, IP: {request.META.get('REMOTE_ADDR')}")
69
- return JsonResponse({"error": str(err) }, status=500)
104
+ if EVENTS_ON_ERRORS:
105
+ rest.MojoModel.class_report_incident_for_user(
106
+ details=f"Rest Exception: {err}",
107
+ event_type="rest_error",
108
+ request_data=request.DATA,
109
+ request=request,
110
+ stack_trace=traceback.format_exc(),
111
+ request_path=getattr(request, "path", None),
112
+ )
113
+ return JsonResponse({"error": str(err), "code": 500 }, status=500)
70
114
 
71
115
  return wrapper
72
116
 
@@ -0,0 +1,45 @@
1
+ """
2
+ AWS Helpers Module
3
+
4
+ A simple interface for working with AWS services.
5
+ """
6
+
7
+ # Import service modules - these will be implemented
8
+ from .s3 import S3Bucket, S3Item
9
+ from .client import get_session
10
+ from .kms import KMSHelper
11
+
12
+ # These will be implemented in future modules
13
+ from .iam import IAMRole, IAMPolicy, IAMUser
14
+ from .ses import EmailSender, EmailTemplate
15
+ from .sns import SNSTopic, SNSSubscription
16
+ from .ec2 import EC2Instance, EC2SecurityGroup
17
+
18
+ __all__ = [
19
+ # Base
20
+ 'get_session',
21
+
22
+ # S3
23
+ 'S3Bucket',
24
+ 'S3Item',
25
+
26
+ # KMS
27
+ 'KMSHelper',
28
+
29
+ # IAM
30
+ 'IAMRole',
31
+ 'IAMPolicy',
32
+ 'IAMUser',
33
+
34
+ # SES
35
+ 'EmailSender',
36
+ 'EmailTemplate',
37
+
38
+ # SNS
39
+ 'SNSTopic',
40
+ 'SNSSubscription',
41
+
42
+ # EC2
43
+ 'EC2Instance',
44
+ 'EC2SecurityGroup',
45
+ ]