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,461 @@
1
+ """
2
+ AWS SNS Helper Module
3
+
4
+ Provides simple interfaces for managing AWS SNS (Simple Notification Service).
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import boto3
10
+ import botocore
11
+ from typing import Dict, List, Optional, Union, Any
12
+
13
+ from .client import get_session
14
+ from mojo.helpers.settings import settings
15
+ from mojo.helpers import logit
16
+
17
+ logger = logit.get_logger(__name__)
18
+
19
+
20
+ class SNSTopic:
21
+ """
22
+ Simple interface for managing SNS topics.
23
+ """
24
+
25
+ def __init__(self, name: str, access_key: Optional[str] = None,
26
+ secret_key: Optional[str] = None, region: Optional[str] = None):
27
+ """
28
+ Initialize a topic manager for the specified SNS topic.
29
+
30
+ Args:
31
+ name: The topic name
32
+ access_key: AWS access key, defaults to settings.AWS_KEY
33
+ secret_key: AWS secret key, defaults to settings.AWS_SECRET
34
+ region: AWS region, defaults to settings.AWS_REGION if available
35
+ """
36
+ self.name = name
37
+ self.access_key = access_key or settings.AWS_KEY
38
+ self.secret_key = secret_key or settings.AWS_SECRET
39
+ self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
40
+
41
+ session = get_session(self.access_key, self.secret_key, self.region)
42
+ self.client = session.client('sns')
43
+ self.arn = None
44
+ self.exists = self._check_exists()
45
+
46
+ def _check_exists(self) -> bool:
47
+ """
48
+ Check if the topic exists and set ARN if it does.
49
+
50
+ Returns:
51
+ True if the topic exists, False otherwise
52
+ """
53
+ try:
54
+ # List topics and check if this one exists
55
+ response = self.client.list_topics()
56
+
57
+ for topic in response.get('Topics', []):
58
+ topic_arn = topic['TopicArn']
59
+ if topic_arn.split(':')[-1] == self.name:
60
+ self.arn = topic_arn
61
+ return True
62
+
63
+ return False
64
+ except botocore.exceptions.ClientError as e:
65
+ logger.error(f"Error checking topic existence: {e}")
66
+ return False
67
+
68
+ def create(self, display_name: Optional[str] = None,
69
+ tags: Optional[List[Dict[str, str]]] = None) -> bool:
70
+ """
71
+ Create an SNS topic.
72
+
73
+ Args:
74
+ display_name: Display name for the topic
75
+ tags: Optional tags for the topic
76
+
77
+ Returns:
78
+ True if topic was created, False if it already exists
79
+ """
80
+ if self.exists:
81
+ logger.info(f"Topic {self.name} already exists")
82
+ return False
83
+
84
+ try:
85
+ # Prepare creation parameters
86
+ create_params = {'Name': self.name}
87
+ attributes = {}
88
+
89
+ if display_name:
90
+ attributes['DisplayName'] = display_name
91
+
92
+ if attributes:
93
+ create_params['Attributes'] = attributes
94
+
95
+ if tags:
96
+ create_params['Tags'] = tags
97
+
98
+ # Create the topic
99
+ response = self.client.create_topic(**create_params)
100
+ self.arn = response['TopicArn']
101
+ self.exists = True
102
+ return True
103
+ except botocore.exceptions.ClientError as e:
104
+ logger.error(f"Failed to create topic {self.name}: {e}")
105
+ return False
106
+
107
+ def delete(self) -> bool:
108
+ """
109
+ Delete the SNS topic.
110
+
111
+ Returns:
112
+ True if successfully deleted, False otherwise
113
+ """
114
+ if not self.exists:
115
+ logger.info(f"Topic {self.name} does not exist")
116
+ return False
117
+
118
+ try:
119
+ self.client.delete_topic(TopicArn=self.arn)
120
+ self.arn = None
121
+ self.exists = False
122
+ return True
123
+ except botocore.exceptions.ClientError as e:
124
+ logger.error(f"Failed to delete topic {self.name}: {e}")
125
+ return False
126
+
127
+ def publish(self, message: str, subject: Optional[str] = None,
128
+ attributes: Optional[Dict[str, Dict]] = None,
129
+ message_structure: Optional[str] = None) -> Dict:
130
+ """
131
+ Publish a message to the topic.
132
+
133
+ Args:
134
+ message: Message to publish
135
+ subject: Optional subject for the message
136
+ attributes: Optional message attributes
137
+ message_structure: Optional message structure (e.g., 'json')
138
+
139
+ Returns:
140
+ Response dict containing MessageId if successful
141
+ """
142
+ if not self.exists:
143
+ logger.warning(f"Topic {self.name} does not exist")
144
+ return {'Error': 'Topic does not exist'}
145
+
146
+ try:
147
+ # Prepare publish parameters
148
+ publish_params = {
149
+ 'TopicArn': self.arn,
150
+ 'Message': message
151
+ }
152
+
153
+ if subject:
154
+ publish_params['Subject'] = subject
155
+
156
+ if attributes:
157
+ publish_params['MessageAttributes'] = attributes
158
+
159
+ if message_structure:
160
+ publish_params['MessageStructure'] = message_structure
161
+
162
+ # Publish the message
163
+ response = self.client.publish(**publish_params)
164
+ logger.info(f"Message published successfully with MessageId: {response['MessageId']}")
165
+ return response
166
+ except botocore.exceptions.ClientError as e:
167
+ logger.error(f"Failed to publish message to topic {self.name}: {e}")
168
+ return {'Error': str(e)}
169
+
170
+ def set_attributes(self, attributes: Dict[str, str]) -> bool:
171
+ """
172
+ Set topic attributes.
173
+
174
+ Args:
175
+ attributes: Dictionary of attribute name-value pairs
176
+
177
+ Returns:
178
+ True if successful, False otherwise
179
+ """
180
+ if not self.exists:
181
+ logger.warning(f"Topic {self.name} does not exist")
182
+ return False
183
+
184
+ try:
185
+ self.client.set_topic_attributes(
186
+ TopicArn=self.arn,
187
+ AttributeName=list(attributes.keys())[0], # Can only set one at a time
188
+ AttributeValue=list(attributes.values())[0]
189
+ )
190
+ return True
191
+ except botocore.exceptions.ClientError as e:
192
+ logger.error(f"Failed to set attributes for topic {self.name}: {e}")
193
+ return False
194
+
195
+ def get_attributes(self) -> Dict[str, str]:
196
+ """
197
+ Get topic attributes.
198
+
199
+ Returns:
200
+ Dictionary of topic attributes
201
+ """
202
+ if not self.exists:
203
+ logger.warning(f"Topic {self.name} does not exist")
204
+ return {}
205
+
206
+ try:
207
+ response = self.client.get_topic_attributes(TopicArn=self.arn)
208
+ return response.get('Attributes', {})
209
+ except botocore.exceptions.ClientError as e:
210
+ logger.error(f"Failed to get attributes for topic {self.name}: {e}")
211
+ return {}
212
+
213
+ def list_subscriptions(self) -> List[Dict]:
214
+ """
215
+ List all subscriptions to this topic.
216
+
217
+ Returns:
218
+ List of subscription dictionaries
219
+ """
220
+ if not self.exists:
221
+ logger.warning(f"Topic {self.name} does not exist")
222
+ return []
223
+
224
+ try:
225
+ response = self.client.list_subscriptions_by_topic(TopicArn=self.arn)
226
+ return response.get('Subscriptions', [])
227
+ except botocore.exceptions.ClientError as e:
228
+ logger.error(f"Failed to list subscriptions for topic {self.name}: {e}")
229
+ return []
230
+
231
+ @staticmethod
232
+ def list_all_topics() -> List[Dict]:
233
+ """
234
+ List all SNS topics.
235
+
236
+ Returns:
237
+ List of topic dictionaries
238
+ """
239
+ client = boto3.client('sns',
240
+ aws_access_key_id=settings.AWS_KEY,
241
+ aws_secret_access_key=settings.AWS_SECRET,
242
+ region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
243
+
244
+ try:
245
+ response = client.list_topics()
246
+ return response.get('Topics', [])
247
+ except botocore.exceptions.ClientError as e:
248
+ logger.error(f"Failed to list topics: {e}")
249
+ return []
250
+
251
+
252
+ class SNSSubscription:
253
+ """
254
+ Simple interface for managing SNS subscriptions.
255
+ """
256
+
257
+ def __init__(self, topic_arn: str, access_key: Optional[str] = None,
258
+ secret_key: Optional[str] = None, region: Optional[str] = None):
259
+ """
260
+ Initialize a subscription manager for the specified SNS topic.
261
+
262
+ Args:
263
+ topic_arn: The ARN of the topic
264
+ access_key: AWS access key, defaults to settings.AWS_KEY
265
+ secret_key: AWS secret key, defaults to settings.AWS_SECRET
266
+ region: AWS region, defaults to settings.AWS_REGION if available
267
+ """
268
+ self.topic_arn = topic_arn
269
+ self.access_key = access_key or settings.AWS_KEY
270
+ self.secret_key = secret_key or settings.AWS_SECRET
271
+ self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
272
+
273
+ session = get_session(self.access_key, self.secret_key, self.region)
274
+ self.client = session.client('sns')
275
+
276
+ def subscribe(self, protocol: str, endpoint: str,
277
+ attributes: Optional[Dict[str, str]] = None,
278
+ return_subscription_arn: bool = False) -> Dict:
279
+ """
280
+ Subscribe an endpoint to the topic.
281
+
282
+ Args:
283
+ protocol: Protocol to use (http, https, email, sms, sqs, application, lambda)
284
+ endpoint: Endpoint to subscribe
285
+ attributes: Optional subscription attributes
286
+ return_subscription_arn: Whether to return the subscription ARN
287
+
288
+ Returns:
289
+ Response dict containing SubscriptionArn
290
+ """
291
+ try:
292
+ # Prepare subscription parameters
293
+ subscribe_params = {
294
+ 'TopicArn': self.topic_arn,
295
+ 'Protocol': protocol,
296
+ 'Endpoint': endpoint,
297
+ 'ReturnSubscriptionArn': return_subscription_arn
298
+ }
299
+
300
+ if attributes:
301
+ subscribe_params['Attributes'] = attributes
302
+
303
+ # Create the subscription
304
+ response = self.client.subscribe(**subscribe_params)
305
+ logger.info(f"Subscription created: {response.get('SubscriptionArn')}")
306
+ return response
307
+ except botocore.exceptions.ClientError as e:
308
+ logger.error(f"Failed to create subscription: {e}")
309
+ return {'Error': str(e)}
310
+
311
+ def unsubscribe(self, subscription_arn: str) -> bool:
312
+ """
313
+ Unsubscribe from the topic.
314
+
315
+ Args:
316
+ subscription_arn: ARN of the subscription to delete
317
+
318
+ Returns:
319
+ True if successful, False otherwise
320
+ """
321
+ try:
322
+ self.client.unsubscribe(SubscriptionArn=subscription_arn)
323
+ logger.info(f"Subscription {subscription_arn} deleted")
324
+ return True
325
+ except botocore.exceptions.ClientError as e:
326
+ logger.error(f"Failed to delete subscription {subscription_arn}: {e}")
327
+ return False
328
+
329
+ def set_attributes(self, subscription_arn: str,
330
+ attribute_name: str, attribute_value: str) -> bool:
331
+ """
332
+ Set subscription attributes.
333
+
334
+ Args:
335
+ subscription_arn: ARN of the subscription
336
+ attribute_name: Name of the attribute to set
337
+ attribute_value: Value of the attribute
338
+
339
+ Returns:
340
+ True if successful, False otherwise
341
+ """
342
+ try:
343
+ self.client.set_subscription_attributes(
344
+ SubscriptionArn=subscription_arn,
345
+ AttributeName=attribute_name,
346
+ AttributeValue=attribute_value
347
+ )
348
+ return True
349
+ except botocore.exceptions.ClientError as e:
350
+ logger.error(f"Failed to set attributes for subscription {subscription_arn}: {e}")
351
+ return False
352
+
353
+ def get_attributes(self, subscription_arn: str) -> Dict[str, str]:
354
+ """
355
+ Get subscription attributes.
356
+
357
+ Args:
358
+ subscription_arn: ARN of the subscription
359
+
360
+ Returns:
361
+ Dictionary of subscription attributes
362
+ """
363
+ try:
364
+ response = self.client.get_subscription_attributes(
365
+ SubscriptionArn=subscription_arn
366
+ )
367
+ return response.get('Attributes', {})
368
+ except botocore.exceptions.ClientError as e:
369
+ logger.error(f"Failed to get attributes for subscription {subscription_arn}: {e}")
370
+ return {}
371
+
372
+ @staticmethod
373
+ def list_all_subscriptions() -> List[Dict]:
374
+ """
375
+ List all SNS subscriptions.
376
+
377
+ Returns:
378
+ List of subscription dictionaries
379
+ """
380
+ client = boto3.client('sns',
381
+ aws_access_key_id=settings.AWS_KEY,
382
+ aws_secret_access_key=settings.AWS_SECRET,
383
+ region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
384
+
385
+ try:
386
+ response = client.list_subscriptions()
387
+ return response.get('Subscriptions', [])
388
+ except botocore.exceptions.ClientError as e:
389
+ logger.error(f"Failed to list subscriptions: {e}")
390
+ return []
391
+
392
+
393
+ # Utility functions
394
+ def create_topic_and_subscribe(topic_name: str, protocol: str, endpoint: str,
395
+ display_name: Optional[str] = None) -> Dict:
396
+ """
397
+ Create a topic and subscribe an endpoint in one operation.
398
+
399
+ Args:
400
+ topic_name: Name of the topic to create
401
+ protocol: Protocol to use for subscription
402
+ endpoint: Endpoint to subscribe
403
+ display_name: Optional display name for the topic
404
+
405
+ Returns:
406
+ Dict with topic ARN and subscription ARN
407
+ """
408
+ result = {}
409
+
410
+ # Create the topic
411
+ topic = SNSTopic(topic_name)
412
+ if not topic.exists:
413
+ if not topic.create(display_name=display_name):
414
+ return {'Error': f"Failed to create topic {topic_name}"}
415
+
416
+ result['TopicArn'] = topic.arn
417
+
418
+ # Create the subscription
419
+ subscription = SNSSubscription(topic.arn)
420
+ response = subscription.subscribe(protocol, endpoint, return_subscription_arn=True)
421
+
422
+ if 'Error' in response:
423
+ result['Error'] = response['Error']
424
+ else:
425
+ result['SubscriptionArn'] = response.get('SubscriptionArn')
426
+
427
+ return result
428
+
429
+
430
+ def publish_message(topic_name: str, message: str, subject: Optional[str] = None) -> Dict:
431
+ """
432
+ Publish a message to a topic by name.
433
+
434
+ Args:
435
+ topic_name: Name of the topic
436
+ message: Message to publish
437
+ subject: Optional message subject
438
+
439
+ Returns:
440
+ Response dict containing MessageId if successful
441
+ """
442
+ topic = SNSTopic(topic_name)
443
+
444
+ if not topic.exists:
445
+ return {'Error': f"Topic {topic_name} does not exist"}
446
+
447
+ return topic.publish(message, subject)
448
+
449
+
450
+ def get_topic_arn(topic_name: str) -> Optional[str]:
451
+ """
452
+ Get the ARN for a topic by name.
453
+
454
+ Args:
455
+ topic_name: Name of the topic
456
+
457
+ Returns:
458
+ Topic ARN if found, None otherwise
459
+ """
460
+ topic = SNSTopic(topic_name)
461
+ return topic.arn if topic.exists else None
@@ -1,4 +1,4 @@
1
1
  from .aes import encrypt, decrypt
2
- from .utils import random_bytes, random_string
2
+ from .utils import random_bytes, random_string, b64_encode, b64_decode
3
3
  from .hash import hash
4
4
  from .sign import generate_signature as sign, verify_signature as verify
@@ -1,5 +1,7 @@
1
1
  from Crypto.Random import get_random_bytes
2
2
  import string
3
+ from base64 import b64encode, b64decode
4
+ import json
3
5
 
4
6
 
5
7
  def generate_key(bit_size=128):
@@ -24,3 +26,16 @@ def random_string(length, allow_digits=True, allow_chars=True, allow_special=Tru
24
26
  raise ValueError("At least one character set (digits, chars, special) must be allowed")
25
27
  random_bytes = get_random_bytes(length)
26
28
  return ''.join(characters[b % len(characters)] for b in random_bytes)
29
+
30
+
31
+ def b64_encode(data):
32
+ if isinstance(data, dict):
33
+ data = json.dumps(data)
34
+ return b64encode(data.encode('utf-8')).decode('utf-8')
35
+
36
+
37
+ def b64_decode(data):
38
+ dec = b64decode(data.encode('utf-8')).decode('utf-8')
39
+ if dec[0] == '{':
40
+ return json.loads(dec)
41
+ return dec
mojo/helpers/dates.py CHANGED
@@ -67,3 +67,21 @@ def subtract(when=None, seconds=None, minutes=None, hours=None, days=None):
67
67
 
68
68
  def has_time_elsapsed(when, seconds=None, minutes=None, hours=None, days=None):
69
69
  return utcnow() >= add(when, seconds, minutes, hours, days)
70
+
71
+
72
+ def is_today(when, timezone=None):
73
+ if timezone is None:
74
+ timezone = 'UTC'
75
+
76
+ # Convert when to the specified timezone
77
+ if when.tzinfo is None:
78
+ when = pytz.UTC.localize(when)
79
+ local_tz = pytz.timezone(timezone)
80
+ when_local = when.astimezone(local_tz)
81
+
82
+ # Get today's date in the specified timezone
83
+ now_utc = utcnow()
84
+ now_local = now_utc.astimezone(local_tz)
85
+
86
+ # Compare dates
87
+ return when_local.date() == now_local.date()
@@ -0,0 +1,2 @@
1
+ # This file marks the 'location' directory as a Python package.
2
+ from .geolocation import geolocate_ip