django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
mojo/helpers/aws/sns.py
ADDED
@@ -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
|
Binary file
|
Binary file
|
Binary file
|
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()
|
mojo/helpers/response.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import
|
1
|
+
from objict import objict
|
2
2
|
from django.http import HttpResponse
|
3
3
|
|
4
4
|
|
@@ -10,5 +10,9 @@ class JsonResponse(HttpResponse):
|
|
10
10
|
'safe parameter to False.'
|
11
11
|
)
|
12
12
|
kwargs.setdefault('content_type', 'application/json')
|
13
|
-
|
13
|
+
if not isinstance(data, objict):
|
14
|
+
data = objict.from_dict(data)
|
15
|
+
if "code" not in data:
|
16
|
+
data.code = status
|
17
|
+
data = data.to_json(as_string=True)
|
14
18
|
super().__init__(content=data, status=status, **kwargs)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import importlib
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
UNKNOWN = Ellipsis
|
5
5
|
|
@@ -16,42 +16,6 @@ def load_settings_profile(context):
|
|
16
16
|
modules.load_module_to_globals("settings.defaults", context)
|
17
17
|
modules.load_module_to_globals(f"settings.{profile}", context)
|
18
18
|
|
19
|
-
def load_settings_config(context):
|
20
|
-
# Load config from django.conf file
|
21
|
-
from mojo.helpers import paths
|
22
|
-
config_path = paths.VAR_ROOT / "django.conf"
|
23
|
-
if not config_path.exists():
|
24
|
-
raise Exception(f"Required configuration file not found: {config_path}")
|
25
|
-
|
26
|
-
with open(config_path, 'r') as file:
|
27
|
-
for line in file:
|
28
|
-
if '=' in line:
|
29
|
-
key, value = line.strip().split('=', 1)
|
30
|
-
value = value.strip()
|
31
|
-
if value.startswith('"') and value.endswith('"'):
|
32
|
-
value = value[1:-1]
|
33
|
-
elif value.startswith("'") and value.endswith("'"):
|
34
|
-
value = value[1:-1]
|
35
|
-
if value.startswith('f"') or value.startswith("f'"):
|
36
|
-
value = eval(value)
|
37
|
-
elif value.lower() == 'true':
|
38
|
-
value = True
|
39
|
-
elif value.lower() == 'false':
|
40
|
-
value = False
|
41
|
-
else:
|
42
|
-
try:
|
43
|
-
if '.' in value:
|
44
|
-
value = float(value)
|
45
|
-
else:
|
46
|
-
value = int(value)
|
47
|
-
except ValueError:
|
48
|
-
pass
|
49
|
-
context[key.strip()] = value
|
50
|
-
|
51
|
-
if context.get("ALLOW_ADMIN_SITE", True):
|
52
|
-
if "django.contrib.admin" not in context["INSTALLED_APPS"]:
|
53
|
-
context["INSTALLED_APPS"].insert(0, "django.contrib.admin")
|
54
|
-
|
55
19
|
|
56
20
|
class SettingsHelper:
|
57
21
|
"""
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class DjangoConfigLoader:
|
2
|
+
"""
|
3
|
+
A clean, expandable class for loading Django configuration from django.conf files.
|
4
|
+
"""
|
5
|
+
|
6
|
+
def __init__(self, config_path=None):
|
7
|
+
"""
|
8
|
+
Initialize the config loader.
|
9
|
+
|
10
|
+
:param config_path: Path to the django.conf file. If None, uses default VAR_ROOT path.
|
11
|
+
"""
|
12
|
+
if config_path is None:
|
13
|
+
from mojo.helpers import paths
|
14
|
+
self.config_path = paths.VAR_ROOT / "django.conf"
|
15
|
+
else:
|
16
|
+
self.config_path = config_path
|
17
|
+
|
18
|
+
def load_config(self, context):
|
19
|
+
"""
|
20
|
+
Load configuration from django.conf file into the provided context.
|
21
|
+
|
22
|
+
:param context: Dictionary to load configuration values into.
|
23
|
+
:raises Exception: If the required configuration file is not found.
|
24
|
+
"""
|
25
|
+
self._validate_config_file()
|
26
|
+
self._parse_config_file(context)
|
27
|
+
self._apply_admin_site_config(context)
|
28
|
+
|
29
|
+
def _validate_config_file(self):
|
30
|
+
"""Validate that the configuration file exists."""
|
31
|
+
if not self.config_path.exists():
|
32
|
+
raise Exception(f"Required configuration file not found: {self.config_path}")
|
33
|
+
|
34
|
+
def _parse_config_file(self, context):
|
35
|
+
"""Parse the configuration file and populate the context."""
|
36
|
+
with open(self.config_path, 'r') as file:
|
37
|
+
for line in file:
|
38
|
+
if '=' in line:
|
39
|
+
key, value = line.strip().split('=', 1)
|
40
|
+
parsed_value = self._parse_value(value.strip())
|
41
|
+
context[key.strip()] = parsed_value
|
42
|
+
|
43
|
+
def _parse_value(self, value):
|
44
|
+
"""
|
45
|
+
Parse a configuration value string into the appropriate Python type.
|
46
|
+
|
47
|
+
:param value: String value to parse.
|
48
|
+
:return: Parsed value with appropriate type.
|
49
|
+
"""
|
50
|
+
if self._is_list_value(value):
|
51
|
+
return self._parse_list_value(value)
|
52
|
+
elif self._is_quoted_string(value):
|
53
|
+
return self._parse_quoted_string(value)
|
54
|
+
elif self._is_f_string(value):
|
55
|
+
return eval(value)
|
56
|
+
elif self._is_boolean(value):
|
57
|
+
return self._parse_boolean(value)
|
58
|
+
else:
|
59
|
+
return self._parse_numeric_or_string(value)
|
60
|
+
|
61
|
+
def _is_list_value(self, value):
|
62
|
+
"""Check if value is a list format."""
|
63
|
+
return value.startswith('[') and value.endswith(']')
|
64
|
+
|
65
|
+
def _is_quoted_string(self, value):
|
66
|
+
"""Check if value is a quoted string."""
|
67
|
+
return ((value.startswith('"') and value.endswith('"')) or
|
68
|
+
(value.startswith("'") and value.endswith("'")))
|
69
|
+
|
70
|
+
def _is_f_string(self, value):
|
71
|
+
"""Check if value is an f-string."""
|
72
|
+
return value.startswith('f"') or value.startswith("f'")
|
73
|
+
|
74
|
+
def _is_boolean(self, value):
|
75
|
+
"""Check if value is a boolean."""
|
76
|
+
return value.lower() in ('true', 'false')
|
77
|
+
|
78
|
+
def _parse_list_value(self, value):
|
79
|
+
"""Parse a list value string into a Python list."""
|
80
|
+
list_content = value[1:-1].strip()
|
81
|
+
if not list_content:
|
82
|
+
return []
|
83
|
+
|
84
|
+
items = []
|
85
|
+
for item in list_content.split(','):
|
86
|
+
item = item.strip()
|
87
|
+
parsed_item = self._parse_list_item(item)
|
88
|
+
items.append(parsed_item)
|
89
|
+
return items
|
90
|
+
|
91
|
+
def _parse_list_item(self, item):
|
92
|
+
"""Parse an individual list item."""
|
93
|
+
if self._is_quoted_string(item):
|
94
|
+
return item[1:-1] # Remove quotes
|
95
|
+
else:
|
96
|
+
return self._parse_numeric_or_string(item)
|
97
|
+
|
98
|
+
def _parse_quoted_string(self, value):
|
99
|
+
"""Parse a quoted string by removing the quotes."""
|
100
|
+
return value[1:-1]
|
101
|
+
|
102
|
+
def _parse_boolean(self, value):
|
103
|
+
"""Parse a boolean string."""
|
104
|
+
return value.lower() == 'true'
|
105
|
+
|
106
|
+
def _parse_numeric_or_string(self, value):
|
107
|
+
"""Parse a value as numeric if possible, otherwise return as string."""
|
108
|
+
try:
|
109
|
+
if '.' in value:
|
110
|
+
return float(value)
|
111
|
+
else:
|
112
|
+
return int(value)
|
113
|
+
except ValueError:
|
114
|
+
return value
|
115
|
+
|
116
|
+
def _apply_admin_site_config(self, context):
|
117
|
+
"""Apply Django admin site configuration if enabled."""
|
118
|
+
if context.get("ALLOW_ADMIN_SITE", True):
|
119
|
+
installed_apps = context.get("INSTALLED_APPS", [])
|
120
|
+
if "django.contrib.admin" not in installed_apps:
|
121
|
+
installed_apps.insert(0, "django.contrib.admin")
|
122
|
+
context["INSTALLED_APPS"] = installed_apps
|
123
|
+
|
124
|
+
|
125
|
+
def load_settings_config(context):
|
126
|
+
"""
|
127
|
+
Load Django configuration from django.conf file.
|
128
|
+
|
129
|
+
:param context: Dictionary to load configuration values into.
|
130
|
+
"""
|
131
|
+
loader = DjangoConfigLoader()
|
132
|
+
loader.load_config(context)
|