django-nativemojo 0.1.10__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.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,202 @@
|
|
1
|
+
from io import StringIO
|
2
|
+
import email
|
3
|
+
import re
|
4
|
+
from email.utils import parseaddr, parsedate_to_datetime, getaddresses
|
5
|
+
from email.header import decode_header
|
6
|
+
from objict import objict
|
7
|
+
|
8
|
+
|
9
|
+
def decode_payload(part):
|
10
|
+
"""
|
11
|
+
Decode the email part payload.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
part (email.message.Part): The email part.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
str: The decoded payload as a string.
|
18
|
+
"""
|
19
|
+
charset = part.get_content_charset() or 'utf-8'
|
20
|
+
return str(part.get_payload(decode=True), charset, 'replace')
|
21
|
+
|
22
|
+
|
23
|
+
def parse_attachment(message_part):
|
24
|
+
"""
|
25
|
+
Parse an email message part for attachments.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
message_part (email.message.Part): The email message part.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
objict: An object representing the attachment, or None if not applicable.
|
32
|
+
"""
|
33
|
+
content_disposition = message_part.get("Content-Disposition")
|
34
|
+
content_type = message_part.get_content_type()
|
35
|
+
|
36
|
+
if not content_disposition and content_type == "multipart/alternative":
|
37
|
+
return None
|
38
|
+
|
39
|
+
attachment = objict({
|
40
|
+
'dispositions': [],
|
41
|
+
'disposition': None,
|
42
|
+
'payload': message_part.get_payload(decode=False),
|
43
|
+
'charset': message_part.get_content_charset(),
|
44
|
+
'content_type': content_type,
|
45
|
+
'encoding': message_part.get("Content-Transfer-Encoding", "utf8"),
|
46
|
+
'content': None,
|
47
|
+
'name': None,
|
48
|
+
'create_date': None,
|
49
|
+
'mod_date': None,
|
50
|
+
'read_date': None,
|
51
|
+
})
|
52
|
+
|
53
|
+
dispositions = []
|
54
|
+
if content_disposition:
|
55
|
+
dispositions = content_disposition.strip().split(";")
|
56
|
+
attachment['dispositions'] = dispositions
|
57
|
+
attachment['disposition'] = dispositions[0]
|
58
|
+
|
59
|
+
if attachment['disposition'] in [None, "inline"] and attachment['content_type'] in ["text/plain", "text/html"]:
|
60
|
+
attachment.content = decode_payload(message_part)
|
61
|
+
|
62
|
+
if content_disposition:
|
63
|
+
for param in dispositions[1:]:
|
64
|
+
name, value = param.split("=")
|
65
|
+
name, value = name.strip().lower(), value.strip().strip('"\'')
|
66
|
+
if name == "filename":
|
67
|
+
attachment.name = value
|
68
|
+
elif name in ["create-date", "creation-date"]:
|
69
|
+
attachment.create_date = value
|
70
|
+
elif name == "modification-date":
|
71
|
+
attachment.mod_date = value
|
72
|
+
elif name == "read-date":
|
73
|
+
attachment.read_date = value
|
74
|
+
|
75
|
+
return attachment
|
76
|
+
|
77
|
+
|
78
|
+
def to_file_object(attachment):
|
79
|
+
"""
|
80
|
+
Convert an attachment to a file-like object.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
attachment (objict): The attachment object.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
StringIO: A StringIO object representing the attachment payload.
|
87
|
+
"""
|
88
|
+
obj = StringIO(to_string(attachment['payload']))
|
89
|
+
obj.name = attachment['name']
|
90
|
+
obj.size = len(attachment['payload']) # 'size' is not typically an attribute of StringIO, but is set here for compatibility
|
91
|
+
return obj
|
92
|
+
|
93
|
+
|
94
|
+
def parse_addresses(input_string, force_name=False, emails_only=False):
|
95
|
+
"""
|
96
|
+
Parse email addresses from a string.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
input_string (str): The input string containing email addresses.
|
100
|
+
force_name (bool): Force inclusion of the domain as the name if no name is present.
|
101
|
+
emails_only (bool): Return only email addresses if True, otherwise return detailed information.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
list: A list of parsed email information or email addresses.
|
105
|
+
"""
|
106
|
+
if not input_string:
|
107
|
+
return []
|
108
|
+
|
109
|
+
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
|
110
|
+
emails = re.findall(email_pattern, input_string)
|
111
|
+
|
112
|
+
parsed_emails = []
|
113
|
+
for addr in emails:
|
114
|
+
addr = addr.strip().lower()
|
115
|
+
name_match = re.search(r'([a-zA-Z\s]+)?\s*<{}>'.format(re.escape(addr)), input_string)
|
116
|
+
name = name_match.group(1).strip() if name_match and name_match.group(1) else (addr.split('@')[1] if force_name else None)
|
117
|
+
full_email = f"{name} <{addr}>" if name else addr
|
118
|
+
parsed_emails.append(objict(name=name, email=addr, full_email=full_email))
|
119
|
+
|
120
|
+
return [email.email for email in parsed_emails] if emails_only else parsed_emails
|
121
|
+
|
122
|
+
|
123
|
+
def to_string(value):
|
124
|
+
"""
|
125
|
+
Convert various types of values to a string.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
value: The value to be converted.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
str: The string representation of the input value.
|
132
|
+
"""
|
133
|
+
if isinstance(value, bytes):
|
134
|
+
return value.decode()
|
135
|
+
if isinstance(value, bytearray):
|
136
|
+
return value.decode("utf-8")
|
137
|
+
if isinstance(value, (int, float)):
|
138
|
+
return str(value)
|
139
|
+
return value
|
140
|
+
|
141
|
+
|
142
|
+
def parse_raw_message(msg_obj):
|
143
|
+
"""
|
144
|
+
Parse an email message and return a dictionary with its components.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
msg_obj (str or email.message.Message): The email message object or its string representation.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
objict: A dictionary-like object containing email components.
|
151
|
+
"""
|
152
|
+
if isinstance(msg_obj, str):
|
153
|
+
msg_obj = email.message_from_string(msg_obj)
|
154
|
+
|
155
|
+
subject, message, body, html = None, None, None, None
|
156
|
+
attachments, body_parts, html_parts = [], [], []
|
157
|
+
|
158
|
+
if msg_obj.get('Subject'):
|
159
|
+
decoded_fragments = decode_header(msg_obj['Subject'])
|
160
|
+
subject = ''.join(
|
161
|
+
str(s, enc or 'utf-8', 'replace')
|
162
|
+
for s, enc in decoded_fragments
|
163
|
+
)
|
164
|
+
|
165
|
+
for part in msg_obj.walk():
|
166
|
+
attachment = parse_attachment(part)
|
167
|
+
if attachment:
|
168
|
+
if attachment.get('content'):
|
169
|
+
(html_parts if attachment['content_type'] == "text/html" else body_parts).append(attachment.content)
|
170
|
+
else:
|
171
|
+
attachments.append(attachment)
|
172
|
+
|
173
|
+
if body_parts:
|
174
|
+
body = ''.join(body_parts).strip()
|
175
|
+
message = '\n'.join(
|
176
|
+
line.strip() for line in body.split('\n') if not line.startswith('>') or (blocks := 0) < 3
|
177
|
+
).strip()
|
178
|
+
|
179
|
+
html = ''.join(html_parts) if html_parts else None
|
180
|
+
|
181
|
+
from_addr = parseaddr(msg_obj.get('From', ''))
|
182
|
+
|
183
|
+
date_time = None
|
184
|
+
if msg_obj.get('Date'):
|
185
|
+
date_time = parsedate_to_datetime(msg_obj['Date'])
|
186
|
+
if date_time:
|
187
|
+
date_time = date_time.replace(tzinfo=None)
|
188
|
+
|
189
|
+
return objict({
|
190
|
+
'subject': subject.strip() if subject else '',
|
191
|
+
'body': body,
|
192
|
+
'sent_at': date_time,
|
193
|
+
'message': message,
|
194
|
+
'html': html,
|
195
|
+
'from_email': from_addr[1],
|
196
|
+
'from_name': from_addr[0],
|
197
|
+
'to': msg_obj.get("To"),
|
198
|
+
'to_addrs': getaddresses(msg_obj.get_all("To", [])),
|
199
|
+
'cc': msg_obj.get("Cc"),
|
200
|
+
'cc_addrs': getaddresses(msg_obj.get_all("Cc", [])),
|
201
|
+
'attachments': attachments,
|
202
|
+
})
|
@@ -0,0 +1,144 @@
|
|
1
|
+
from django.template.loader import render_to_string
|
2
|
+
from email.mime.multipart import MIMEMultipart
|
3
|
+
from email.mime.text import MIMEText
|
4
|
+
from email.mime.application import MIMEApplication
|
5
|
+
from mojo.helpers.settings import settings
|
6
|
+
import os
|
7
|
+
from objict import objict
|
8
|
+
from mailman.models.template import MailTemplate
|
9
|
+
import csv
|
10
|
+
from io import StringIO
|
11
|
+
import re
|
12
|
+
|
13
|
+
|
14
|
+
def create_message(sender, recipients, subject, text=None, html=None, attachments=None, replyto=None):
|
15
|
+
"""
|
16
|
+
Prepares an email message with given parameters.
|
17
|
+
|
18
|
+
:param sender: The sender of the email.
|
19
|
+
:param recipients: Can be a single email (string), or multiple emails (list or tuple).
|
20
|
+
:param subject: Subject of the email.
|
21
|
+
:param text: Plain text content of the email.
|
22
|
+
:param html: HTML content of the email.
|
23
|
+
:param attachments: List of attachments, can be strings or bytes.
|
24
|
+
:param replyto: Email for reply-to address.
|
25
|
+
:return: An object containing email details.
|
26
|
+
"""
|
27
|
+
recipients = _parse_recipients(recipients)
|
28
|
+
attachments = attachments or []
|
29
|
+
|
30
|
+
message = objict(sender=sender, recipients=recipients)
|
31
|
+
message.msg = create_multipart_message(sender, recipients, subject, text, html, attachments, replyto)
|
32
|
+
return message
|
33
|
+
|
34
|
+
def _parse_recipients(recipients):
|
35
|
+
"""
|
36
|
+
Parses recipients input into a list of emails.
|
37
|
+
|
38
|
+
:param recipients: Can be a single email (string) or delimited string (comma/semicolon), or a list/tuple.
|
39
|
+
:return: List of email strings.
|
40
|
+
"""
|
41
|
+
if isinstance(recipients, str):
|
42
|
+
if ',' in recipients:
|
43
|
+
recipients = [t.strip() for t in recipients.split(',')]
|
44
|
+
elif ';' in recipients:
|
45
|
+
recipients = [t.strip() for t in recipients.split(';')]
|
46
|
+
else:
|
47
|
+
recipients = [recipients]
|
48
|
+
elif not isinstance(recipients, (tuple, list)):
|
49
|
+
recipients = [recipients]
|
50
|
+
return recipients
|
51
|
+
|
52
|
+
def create_multipart_message(sender, recipients, subject, text=None, html=None, attachments=None, replyto=None):
|
53
|
+
"""
|
54
|
+
Helper function to create a MIME multipart message with text, HTML, and attachments.
|
55
|
+
|
56
|
+
:param sender: Email sender.
|
57
|
+
:param recipients: List of recipient emails.
|
58
|
+
:param subject: Subject of the email.
|
59
|
+
:param text: Text content of the email.
|
60
|
+
:param html: HTML content of the email.
|
61
|
+
:param attachments: List of attachments.
|
62
|
+
:param replyto: Reply-to email address.
|
63
|
+
:return: A prepared MIMEMultipart message.
|
64
|
+
"""
|
65
|
+
multipart_content_subtype = 'alternative' if text and html else 'mixed'
|
66
|
+
msg = MIMEMultipart(multipart_content_subtype)
|
67
|
+
msg['Subject'] = subject
|
68
|
+
msg['From'] = sender
|
69
|
+
msg['To'] = ', '.join(recipients)
|
70
|
+
if replyto:
|
71
|
+
msg.add_header('reply-to', replyto)
|
72
|
+
|
73
|
+
if text:
|
74
|
+
msg.attach(MIMEText(text, 'plain'))
|
75
|
+
if html:
|
76
|
+
# Remove non-ASCII characters to prevent encoding issues
|
77
|
+
html = html.encode('ascii', 'ignore').decode('ascii')
|
78
|
+
msg.attach(MIMEText(html, 'html'))
|
79
|
+
|
80
|
+
for index, atch in enumerate(attachments or [], start=1):
|
81
|
+
if isinstance(atch, (str, bytes)):
|
82
|
+
atch = objict(name=f"attachment{index}.txt", data=atch, mimetype="text/plain")
|
83
|
+
part = MIMEApplication(atch.data)
|
84
|
+
part.add_header('Content-Type', atch.mimetype)
|
85
|
+
part.add_header('Content-Disposition', 'attachment', filename=os.path.basename(atch.name))
|
86
|
+
msg.attach(part)
|
87
|
+
|
88
|
+
return msg
|
89
|
+
|
90
|
+
def render_template(template_name, context, group=None):
|
91
|
+
"""
|
92
|
+
Renders an email template with a provided context.
|
93
|
+
|
94
|
+
:param template_name: Name of the template.
|
95
|
+
:param context: Context to render the template.
|
96
|
+
:param group: Template group filter (optional).
|
97
|
+
:return: Rendered template as string or None.
|
98
|
+
"""
|
99
|
+
context.update({
|
100
|
+
"SITE_LABEL": settings.SITE_LABEL,
|
101
|
+
"BASE_URL": settings.BASE_URL,
|
102
|
+
"SITE_LOGO": settings.SITE_LOGO,
|
103
|
+
"SERVER_NAME": settings.SERVER_NAME,
|
104
|
+
"UNSUBSCRIBE_URL": settings.get("UNSUBSCRIBE_URL", f"{settings.BASE_URL}/api/account/unsubscribe"),
|
105
|
+
"version": settings.VERSION,
|
106
|
+
"COMPANY_NAME": settings.get("COMPANY_NAME", context.get("COMPANY_NAME", ""))
|
107
|
+
})
|
108
|
+
|
109
|
+
if template_name.endswith(("html", ".txt")):
|
110
|
+
return render_to_string(template_name, context)
|
111
|
+
|
112
|
+
qset = MailTemplate.objects.filter(name=template_name)
|
113
|
+
if group is not None:
|
114
|
+
qset = qset.filter(group=group)
|
115
|
+
|
116
|
+
mtemp = qset.last()
|
117
|
+
return mtemp.render(context) if mtemp else None
|
118
|
+
|
119
|
+
def generate_csv(qset, fields, name):
|
120
|
+
"""
|
121
|
+
Generates a CSV from a queryset.
|
122
|
+
|
123
|
+
:param qset: Queryset containing data.
|
124
|
+
:param fields: Fields to include in CSV.
|
125
|
+
:param name: Name for the CSV file.
|
126
|
+
:return: An object with CSV data and metadata.
|
127
|
+
"""
|
128
|
+
csv_io = StringIO()
|
129
|
+
csvwriter = csv.writer(csv_io)
|
130
|
+
|
131
|
+
csvwriter.writerow(fields)
|
132
|
+
for row in qset.values_list(*fields):
|
133
|
+
csvwriter.writerow(map(str, row))
|
134
|
+
|
135
|
+
return objict(name=name, file=csv_io, data=csv_io.getvalue(), mimetype="text/csv")
|
136
|
+
|
137
|
+
def is_html(text):
|
138
|
+
"""
|
139
|
+
Determine if the provided string contains HTML.
|
140
|
+
|
141
|
+
:param text: The text to be checked.
|
142
|
+
:return: True if HTML tags are found, False otherwise.
|
143
|
+
"""
|
144
|
+
return bool(re.search(r'<[a-zA-Z0-9]+>.*?<\/[a-zA-Z0-9]+>', text))
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# Taskit
|
2
|
+
|
3
|
+
Taskit is a task management and processing library that uses Redis to handle task states across multiple channels. It provides a flexible and efficient way to publish, execute, and track tasks. Taskit is built with Django and is intended to be used in projects where distributed or asynchronous task execution is required.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Features](#features)
|
8
|
+
- [Installation](#installation)
|
9
|
+
- [Configuration](#configuration)
|
10
|
+
- [Usage](#usage)
|
11
|
+
- [Publishing Tasks](#publishing-tasks)
|
12
|
+
- [Monitoring Tasks](#monitoring-tasks)
|
13
|
+
- [Advanced Usage](#advanced-usage)
|
14
|
+
- [API Endpoints](#api-endpoints)
|
15
|
+
- [Command Line Interface](#command-line-interface)
|
16
|
+
- [License](#license)
|
17
|
+
|
18
|
+
## Features
|
19
|
+
|
20
|
+
- Asynchronous task management using Redis-backed storage.
|
21
|
+
- Support for multiple channels to organize and prioritize tasks.
|
22
|
+
- Task lifecycle management including pending, running, completed, and error states.
|
23
|
+
- Built-in REST API for monitoring task statuses.
|
24
|
+
- Thread pool executor for concurrent task execution.
|
25
|
+
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
Ensure your Django project is set up to use Taskit by configuring the necessary settings and Redis connection. Update your `settings.py` file:
|
30
|
+
|
31
|
+
```python
|
32
|
+
# settings.py
|
33
|
+
|
34
|
+
TASKIT_CHANNELS = ['channel1', 'channel2', 'broadcast']
|
35
|
+
REDIS_HOST = 'localhost'
|
36
|
+
REDIS_PORT = 6379
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Publishing Tasks
|
42
|
+
|
43
|
+
To publish a new task, use the `TaskManager` instance provided by Taskit:
|
44
|
+
|
45
|
+
```python
|
46
|
+
from taskit.manager import TaskManager
|
47
|
+
|
48
|
+
channels = ['channel1', 'channel2']
|
49
|
+
task_manager = TaskManager(channels)
|
50
|
+
|
51
|
+
def my_task_function(task_data):
|
52
|
+
data = task_data.data
|
53
|
+
print(f"Processing data: {data}")
|
54
|
+
|
55
|
+
task_data = {'key': 'value'}
|
56
|
+
task_id = task_manager.publish('module.my_task_function', task_data, channel='channel1')
|
57
|
+
|
58
|
+
print(f"Task {task_id} published to channel1")
|
59
|
+
```
|
60
|
+
|
61
|
+
### Monitoring Tasks
|
62
|
+
|
63
|
+
Taskit includes a REST API for checking the status of tasks:
|
64
|
+
|
65
|
+
- `GET /status` - Summary of all tasks across all channels.
|
66
|
+
- `GET /pending` - List of pending tasks.
|
67
|
+
- `GET /completed` - List of completed tasks.
|
68
|
+
- `GET /running` - List of tasks currently running.
|
69
|
+
- `GET /errors` - List of tasks that encountered errors.
|
70
|
+
|
71
|
+
### Advanced Usage
|
72
|
+
|
73
|
+
Use the `TaskEngine` to start the task processing engine. This engine will listen for tasks on the specified channels and execute them using a thread pool:
|
74
|
+
|
75
|
+
```python
|
76
|
+
from taskit.runner import TaskEngine
|
77
|
+
|
78
|
+
engine = TaskEngine(['channel1', 'channel2'], max_workers=10)
|
79
|
+
engine.run()
|
80
|
+
```
|
81
|
+
|
82
|
+
You can also leverage the daemon capabilities by using CLI commands like `--start` and `--stop` to manage the task engine:
|
83
|
+
|
84
|
+
```sh
|
85
|
+
python manage.py taskit --start
|
86
|
+
```
|
87
|
+
|
88
|
+
## API Endpoints
|
89
|
+
|
90
|
+
Taskit provides several Django REST API endpoints for managing tasks:
|
91
|
+
|
92
|
+
- **Status:** `GET /status/` - Get the overall status of the task system.
|
93
|
+
- **Pending Tasks:** `GET /pending/` - Retrieve all pending tasks.
|
94
|
+
- **Completed Tasks:** `GET /completed/` - Retrieve all completed tasks.
|
95
|
+
- **Running Tasks:** `GET /running/` - Retrieve all running tasks.
|
96
|
+
- **Error Tasks:** `GET /errors/` - Retrieve tasks that encountered errors.
|
97
|
+
|
98
|
+
## Command Line Interface
|
99
|
+
|
100
|
+
Taskit also offers CLI support for managing the task engine:
|
101
|
+
|
102
|
+
```sh
|
103
|
+
python manage.py taskit --help
|
104
|
+
```
|
105
|
+
|
106
|
+
Options:
|
107
|
+
- `--start`: Start the task engine daemon.
|
108
|
+
- `--stop`: Stop the task engine daemon.
|
109
|
+
- `-f, --foreground`: Run the task engine in the foreground.
|
110
|
+
- `-v, --verbose`: Enable verbose logging for more detailed output.
|
111
|
+
|
112
|
+
## License
|
113
|
+
|
114
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
115
|
+
|
116
|
+
---
|
117
|
+
|
118
|
+
This README file provides a comprehensive overview and instructions for using Taskit. Please feel free to ask further questions or raise issues in the GitHub repository.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from mojo.helpers.settings import settings
|
2
|
+
|
3
|
+
|
4
|
+
def get_manager():
|
5
|
+
from .manager import TaskManager
|
6
|
+
return TaskManager(settings.TASKIT_CHANNELS)
|
7
|
+
|
8
|
+
|
9
|
+
def publish(channel, function, data, expires=1800):
|
10
|
+
man = get_manager()
|
11
|
+
return man.publish(function, data, channel=channel, expires=expires)
|