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
@@ -1,6 +1,7 @@
1
1
  from mojo.models import MojoModel
2
2
  from django.db import models as dm
3
3
  from mojo.helpers import logit
4
+ import ujson
4
5
  # logger = logit.get_logger("requests", "requests.log")
5
6
 
6
7
 
@@ -23,12 +24,15 @@ class Log(dm.Model, MojoModel):
23
24
 
24
25
  @classmethod
25
26
  def logit(cls, request, log, kind="log", model_name=None, model_id=0, level="info", **kwargs):
27
+ if isinstance(log, dict):
28
+ log = ujson.encode(log, indent=4)
26
29
  if not isinstance(log, (bytes, str)):
27
30
  log = f"INVALID LOG TYPE: attempting to log type: {type(log)}"
28
31
  log = log.decode("utf-8") if isinstance(log, bytes) else log
29
32
  log = logit.mask_sensitive_data(log)
30
33
 
31
34
  uid, username, ip_address, path, method, duid = 0, None, None, None, None, None
35
+ user_agent = "system"
32
36
  if request:
33
37
  username = request.user.username if request.user.is_authenticated else None
34
38
  uid = request.user.pk if request.user.is_authenticated else 0
@@ -36,6 +40,7 @@ class Log(dm.Model, MojoModel):
36
40
  duid = request.duid
37
41
  ip_address = request.ip
38
42
  method = request.method
43
+ user_agent = request.user_agent
39
44
 
40
45
  path = kwargs.get("path", path)
41
46
  method = kwargs.get("method", method)
@@ -46,12 +51,13 @@ class Log(dm.Model, MojoModel):
46
51
  kind=kind,
47
52
  method=method,
48
53
  path=path,
54
+ payload=kwargs.get("payload", None),
49
55
  ip=ip_address,
50
56
  uid=uid,
51
57
  duid=duid,
52
58
  username=username,
53
59
  log=log,
54
- user_agent=request.user_agent,
60
+ user_agent=user_agent,
55
61
  model_name=model_name,
56
62
  model_id=model_id
57
63
  )
@@ -1,6 +1,7 @@
1
1
  from .redis_metrics import (
2
2
  record,
3
3
  fetch,
4
+ fetch_values,
4
5
  get_categories,
5
6
  fetch_by_category,
6
7
  get_category_slugs,
@@ -8,5 +9,11 @@ from .redis_metrics import (
8
9
  get_view_perms,
9
10
  get_write_perms,
10
11
  set_view_perms,
11
- set_write_perms
12
+ set_write_perms,
13
+ list_accounts,
14
+ add_account,
15
+ delete_account,
16
+ get_accounts_with_permissions,
17
+ set_value,
18
+ get_value
12
19
  )
@@ -84,6 +84,104 @@ def fetch(slug, dt_start=None, dt_end=None, granularity="hours",
84
84
  return values
85
85
  return nobjict(labels=utils.periods_from_dr_slugs(dr_slugs), data={slug: values})
86
86
 
87
+
88
+ def fetch_values(slugs, when=None, granularity="hours", redis_con=None, account="global", timezone=None):
89
+ """
90
+ Fetches specific metric values for multiple slugs at a single point in time.
91
+
92
+ Args:
93
+ slugs (str or list): The slug(s) to fetch values for. If string, should be comma-separated.
94
+ when (datetime): The specific datetime to fetch values for.
95
+ granularity (str, optional): The time granularity to use. Defaults to "hours".
96
+ redis_con: The Redis connection instance (optional).
97
+ account (str, optional): The account under which the metrics are recorded. Defaults to "global".
98
+
99
+ Returns:
100
+ dict: A dictionary containing:
101
+ - 'data': dict mapping slug names to their values
102
+ - 'slugs': list of slug names that were queried
103
+ - 'when': the datetime that was queried (as string)
104
+ - 'granularity': the granularity used
105
+ - 'account': the account queried
106
+ """
107
+ if redis_con is None:
108
+ redis_con = redis.get_connection()
109
+ when = utils.normalize_datetime(when, timezone)
110
+ # Handle comma-separated string input
111
+ if isinstance(slugs, str):
112
+ if ',' in slugs:
113
+ slugs = [s.strip() for s in slugs.split(',')]
114
+ else:
115
+ slugs = [slugs]
116
+
117
+ # Generate Redis keys for each slug at the specific datetime
118
+ redis_keys = []
119
+ for slug in slugs:
120
+ redis_key = utils.generate_slug(slug, when, granularity, account)
121
+ redis_keys.append(redis_key)
122
+
123
+ # Fetch all values with single MGET operation
124
+ values = redis_con.mget(redis_keys)
125
+
126
+ # Build response data dictionary
127
+ data = {}
128
+ for i, slug in enumerate(slugs):
129
+ value = values[i]
130
+ data[slug] = int(value) if value is not None else 0
131
+
132
+ return {
133
+ 'data': data,
134
+ 'slugs': slugs,
135
+ 'when': when.isoformat() if hasattr(when, 'isoformat') else str(when),
136
+ 'granularity': granularity,
137
+ 'account': account
138
+ }
139
+
140
+
141
+ def set_value(slug, value, redis_con=None, account="global"):
142
+ """
143
+ Sets a simple key-value pair in Redis for global storage (not time-series).
144
+
145
+ Args:
146
+ slug (str): The key identifier for the value.
147
+ value: The value to store (will be converted to string).
148
+ redis_con: The Redis connection instance (optional).
149
+ account (str, optional): The account under which the value is stored. Defaults to "global".
150
+
151
+ Returns:
152
+ None
153
+ """
154
+ if redis_con is None:
155
+ redis_con = redis.get_connection()
156
+
157
+ key = utils.generate_value_key(slug, account)
158
+ redis_con.set(key, str(value))
159
+
160
+
161
+ def get_value(slug, redis_con=None, account="global", default=None):
162
+ """
163
+ Retrieves a simple value from Redis for global storage (not time-series).
164
+
165
+ Args:
166
+ slug (str): The key identifier for the value.
167
+ redis_con: The Redis connection instance (optional).
168
+ account (str, optional): The account under which the value is stored. Defaults to "global".
169
+ default: The default value to return if key doesn't exist. Defaults to None.
170
+
171
+ Returns:
172
+ str or default: The stored value as string, or default if key doesn't exist.
173
+ """
174
+ if redis_con is None:
175
+ redis_con = redis.get_connection()
176
+
177
+ key = utils.generate_value_key(slug, account)
178
+ value = redis_con.get(key)
179
+
180
+ if value is not None:
181
+ return value.decode('utf-8')
182
+ return default
183
+
184
+
87
185
  def add_metrics_slug(slug, redis_con=None, account="global"):
88
186
  """
89
187
  Adds a metric slug to a Redis set for the specified account.
@@ -259,6 +357,7 @@ def set_view_perms(account, perms, redis_con=None):
259
357
  if perms is None:
260
358
  redis_con.delete(view_perm_key)
261
359
  else:
360
+ add_account(account, redis_con)
262
361
  if isinstance(perms, list):
263
362
  perms = ','.join(perms)
264
363
  redis_con.set(view_perm_key, perms)
@@ -282,6 +381,7 @@ def set_write_perms(account, perms, redis_con=None):
282
381
  if perms is None:
283
382
  redis_con.delete(write_perm_key)
284
383
  else:
384
+ add_account(account, redis_con)
285
385
  if isinstance(perms, list):
286
386
  perms = ','.join(perms)
287
387
  redis_con.set(write_perm_key, perms)
@@ -329,3 +429,101 @@ def get_write_perms(account, redis_con=None):
329
429
  if ',' in perms:
330
430
  perms = perms.split(',')
331
431
  return perms
432
+
433
+ def add_account(account, redis_con=None):
434
+ """
435
+ Adds a new account to the system.
436
+
437
+ Args:
438
+ account (str): The account to add.
439
+ redis_con: The Redis connection instance (optional).
440
+
441
+ Returns:
442
+ bool: True if the account was added successfully, False otherwise.
443
+ """
444
+ if redis_con is None:
445
+ redis_con = redis.get_connection()
446
+ accounts_key = utils.generate_accounts_key()
447
+ return redis_con.sadd(accounts_key, account) == 1
448
+
449
+ def list_accounts(redis_con=None):
450
+ """
451
+ Lists all accounts in the system.
452
+
453
+ Args:
454
+ redis_con: The Redis connection instance (optional).
455
+
456
+ Returns:
457
+ list: A list of all accounts in the system.
458
+ """
459
+ if redis_con is None:
460
+ redis_con = redis.get_connection()
461
+ accounts_key = utils.generate_accounts_key()
462
+ return [account.decode('utf-8') for account in redis_con.smembers(accounts_key)]
463
+
464
+ def delete_account(account, redis_con=None):
465
+ if redis_con is None:
466
+ redis_con = redis.get_connection()
467
+ set_view_perms(account, None, redis_con)
468
+ set_write_perms(account, None, redis_con)
469
+ accounts_key = utils.generate_accounts_key()
470
+ return redis_con.srem(accounts_key, account)
471
+
472
+
473
+
474
+ def get_accounts_with_permissions(redis_con=None):
475
+ """
476
+ Scans Redis to find all accounts that have view or write permissions configured.
477
+
478
+ Args:
479
+ redis_con: The Redis connection instance (optional).
480
+
481
+ Returns:
482
+ list: A list of dictionaries containing account information and their permissions.
483
+ """
484
+ if redis_con is None:
485
+ redis_con = redis.get_connection()
486
+
487
+ accounts = {}
488
+
489
+ # Scan for view permission keys
490
+ cursor = b'0'
491
+ while cursor != b'0':
492
+ cursor, keys = redis_con.scan(cursor=cursor, match="mets:*:perm:v")
493
+ for key in keys:
494
+ key_str = key.decode('utf-8')
495
+ # Extract account from key: mets:{account}:perm:v
496
+ parts = key_str.split(':')
497
+ if len(parts) >= 4:
498
+ account = parts[1]
499
+ if account not in accounts:
500
+ accounts[account] = {"account": account, "view_permissions": None, "write_permissions": None}
501
+
502
+ perms = redis_con.get(key)
503
+ if perms:
504
+ perms = perms.decode('utf-8')
505
+ if ',' in perms:
506
+ perms = perms.split(',')
507
+ accounts[account]["view_permissions"] = perms
508
+
509
+ # Scan for write permission keys
510
+ cursor = b'0'
511
+ while cursor != b'0':
512
+ cursor, keys = redis_con.scan(cursor=cursor, match="mets:*:perm:w")
513
+ for key in keys:
514
+ key_str = key.decode('utf-8')
515
+ # Extract account from key: mets:{account}:perm:w
516
+ parts = key_str.split(':')
517
+ if len(parts) >= 4:
518
+ account = parts[1]
519
+ if account not in accounts:
520
+ accounts[account] = {"account": account, "view_permissions": None, "write_permissions": None}
521
+
522
+ perms = redis_con.get(key)
523
+ if perms:
524
+ perms = perms.decode('utf-8')
525
+ if ',' in perms:
526
+ perms = perms.split(',')
527
+ accounts[account]["write_permissions"] = perms
528
+
529
+ return list(accounts.values())
@@ -1 +1,4 @@
1
1
  from .base import *
2
+ from .values import *
3
+ from .categories import *
4
+ from .permissions import *
@@ -0,0 +1,266 @@
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 API endpoints
9
+ CATEGORIES_LIST_DOCS = {
10
+ "summary": "List all categories",
11
+ "description": "Retrieves all categories for a specific account.",
12
+ "parameters": [
13
+ {
14
+ "name": "account",
15
+ "in": "query",
16
+ "schema": {"type": "string", "default": "public"},
17
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
18
+ }
19
+ ],
20
+ "responses": {
21
+ "200": {
22
+ "description": "Successful response with list of categories.",
23
+ "content": {
24
+ "application/json": {
25
+ "example": {
26
+ "response": {
27
+ "categories": ["activity", "engagement", "performance"],
28
+ "account": "public",
29
+ "status": True
30
+ },
31
+ "status_code": 200
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ CATEGORY_SLUGS_DOCS = {
40
+ "summary": "Get slugs in a category",
41
+ "description": "Retrieves all slugs within a specific category.",
42
+ "parameters": [
43
+ {
44
+ "name": "category",
45
+ "in": "query",
46
+ "required": True,
47
+ "schema": {"type": "string"},
48
+ "description": "The category name to get slugs for."
49
+ },
50
+ {
51
+ "name": "account",
52
+ "in": "query",
53
+ "schema": {"type": "string", "default": "public"},
54
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
55
+ }
56
+ ],
57
+ "responses": {
58
+ "200": {
59
+ "description": "Successful response with list of slugs in the category.",
60
+ "content": {
61
+ "application/json": {
62
+ "example": {
63
+ "response": {
64
+ "slugs": ["user_login", "user_signup", "user_logout"],
65
+ "category": "activity",
66
+ "account": "public",
67
+ "status": True
68
+ },
69
+ "status_code": 200
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ CATEGORY_FETCH_DOCS = {
78
+ "summary": "Fetch metrics for category",
79
+ "description": "Retrieves metrics data for all slugs within a category, date range, and granularity.",
80
+ "parameters": [
81
+ {
82
+ "name": "category",
83
+ "in": "query",
84
+ "required": True,
85
+ "schema": {"type": "string"},
86
+ "description": "The category name to fetch metrics for."
87
+ },
88
+ {
89
+ "name": "dt_start",
90
+ "in": "query",
91
+ "schema": {"type": "string", "format": "date-time"},
92
+ "description": "Start date and time for the data range."
93
+ },
94
+ {
95
+ "name": "dt_end",
96
+ "in": "query",
97
+ "schema": {"type": "string", "format": "date-time"},
98
+ "description": "End date and time for the data range."
99
+ },
100
+ {
101
+ "name": "account",
102
+ "in": "query",
103
+ "schema": {"type": "string", "default": "public"},
104
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
105
+ },
106
+ {
107
+ "name": "granularity",
108
+ "in": "query",
109
+ "schema": {"type": "string", "default": "hours"},
110
+ "description": "Granularity of the data (e.g., 'hours', 'days', 'months', 'years')."
111
+ },
112
+ {
113
+ "name": "with_labels",
114
+ "in": "query",
115
+ "schema": {"type": "boolean", "default": False},
116
+ "description": "Include timestamp labels in response data."
117
+ }
118
+ ],
119
+ "responses": {
120
+ "200": {
121
+ "description": "Successful response with category metrics data.",
122
+ "content": {
123
+ "application/json": {
124
+ "example": {
125
+ "response": {
126
+ "data": {
127
+ "user_login": [1, 2, 3, 4],
128
+ "user_signup": [0, 1, 0, 2]
129
+ },
130
+ "category": "activity",
131
+ "account": "public",
132
+ "status": True
133
+ },
134
+ "status_code": 200
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ CATEGORY_DELETE_DOCS = {
143
+ "summary": "Delete category",
144
+ "description": "Deletes an entire category and all its associated slugs and data.",
145
+ "parameters": [
146
+ {
147
+ "name": "category",
148
+ "in": "body",
149
+ "required": True,
150
+ "schema": {"type": "string"},
151
+ "description": "The category name to delete."
152
+ },
153
+ {
154
+ "name": "account",
155
+ "in": "body",
156
+ "schema": {"type": "string", "default": "public"},
157
+ "description": "Account identifier (e.g., 'public', 'global', or 'group_<id>')."
158
+ }
159
+ ],
160
+ "responses": {
161
+ "200": {
162
+ "description": "Successful deletion of category.",
163
+ "content": {
164
+ "application/json": {
165
+ "example": {
166
+ "response": {
167
+ "deleted_category": "activity",
168
+ "account": "public",
169
+ "status": True
170
+ },
171
+ "status_code": 200
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+
180
+ @md.GET('categories', docs=CATEGORIES_LIST_DOCS)
181
+ def on_categories_list(request):
182
+ """
183
+ List all categories for an account.
184
+ """
185
+ account = request.DATA.get("account", "public")
186
+ check_view_permissions(request, account)
187
+
188
+ categories = list(metrics.get_categories(account=account))
189
+
190
+ return JsonResponse({
191
+ "categories": categories,
192
+ "account": account,
193
+ "status": True
194
+ })
195
+
196
+
197
+ @md.GET('category_slugs', docs=CATEGORY_SLUGS_DOCS)
198
+ @md.requires_params("category")
199
+ def on_category_slugs(request):
200
+ """
201
+ Get all slugs within a specific category.
202
+ """
203
+ category = request.DATA.get("category")
204
+ account = request.DATA.get("account", "public")
205
+ check_view_permissions(request, account)
206
+
207
+ slugs = list(metrics.get_category_slugs(category, account=account))
208
+
209
+ return JsonResponse({
210
+ "slugs": slugs,
211
+ "category": category,
212
+ "account": account,
213
+ "status": True
214
+ })
215
+
216
+
217
+ @md.GET('category_fetch', docs=CATEGORY_FETCH_DOCS)
218
+ @md.requires_params("category")
219
+ def on_category_fetch(request):
220
+ """
221
+ Fetch metrics for all slugs within a category.
222
+ """
223
+ category = request.DATA.get("category")
224
+ dt_start = request.DATA.get_typed("dt_start", typed=datetime.datetime)
225
+ dt_end = request.DATA.get_typed("dt_end", typed=datetime.datetime)
226
+ account = request.DATA.get("account", "public")
227
+ granularity = request.DATA.get("granularity", "hours")
228
+ with_labels = request.DATA.get_typed("with_labels", default=False, typed=bool)
229
+
230
+ check_view_permissions(request, account)
231
+
232
+ data = metrics.fetch_by_category(
233
+ category,
234
+ dt_start=dt_start,
235
+ dt_end=dt_end,
236
+ granularity=granularity,
237
+ account=account,
238
+ with_labels=with_labels
239
+ )
240
+
241
+ return JsonResponse({
242
+ "data": data,
243
+ "category": category,
244
+ "account": account,
245
+ "status": True
246
+ })
247
+
248
+
249
+ @md.DELETE('category_delete', docs=CATEGORY_DELETE_DOCS)
250
+ @md.requires_params("category")
251
+ def on_category_delete(request):
252
+ """
253
+ Delete an entire category and all its associated slugs and data.
254
+ """
255
+ category = request.DATA.get("category")
256
+ account = request.DATA.get("account", "public")
257
+
258
+ check_write_permissions(request, account)
259
+
260
+ metrics.delete_category(category, account=account)
261
+
262
+ return JsonResponse({
263
+ "deleted_category": category,
264
+ "account": account,
265
+ "status": True
266
+ })
@@ -0,0 +1,48 @@
1
+ from mojo.apps import metrics
2
+ import mojo.errors
3
+
4
+
5
+ def check_view_permissions(request, account="public"):
6
+ """
7
+ Helper function to check view permissions for metrics operations.
8
+
9
+ Args:
10
+ request: The Django request object
11
+ account: The account to check permissions for
12
+
13
+ Raises:
14
+ PermissionDeniedException: If user doesn't have proper permissions
15
+ """
16
+ if account == "global":
17
+ if not request.user.is_authenticated or not request.user.has_permission("view_metrics"):
18
+ raise mojo.errors.PermissionDeniedException()
19
+ elif account != "public":
20
+ perms = metrics.get_view_perms(account)
21
+ if not perms:
22
+ raise mojo.errors.PermissionDeniedException()
23
+ if perms != "public":
24
+ if not request.user.is_authenticated or not request.user.has_permission(perms):
25
+ raise mojo.errors.PermissionDeniedException()
26
+
27
+
28
+ def check_write_permissions(request, account="public"):
29
+ """
30
+ Helper function to check write permissions for metrics operations.
31
+
32
+ Args:
33
+ request: The Django request object
34
+ account: The account to check permissions for
35
+
36
+ Raises:
37
+ PermissionDeniedException: If user doesn't have proper permissions
38
+ """
39
+ if account == "global":
40
+ if not request.user.is_authenticated or not request.user.has_permission("write_metrics"):
41
+ raise mojo.errors.PermissionDeniedException()
42
+ elif account != "public":
43
+ perms = metrics.get_write_perms(account)
44
+ if not perms:
45
+ raise mojo.errors.PermissionDeniedException()
46
+ if perms != "public":
47
+ if not request.user.is_authenticated or not request.user.has_permission(perms):
48
+ raise mojo.errors.PermissionDeniedException()