epok-toolkit 0.1.0__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.
Potentially problematic release.
This version of epok-toolkit might be problematic. Click here for more details.
- epok_toolkit/__init__.py +0 -0
- epok_toolkit/apps.py +14 -0
- epok_toolkit/default_settings.py +3 -0
- epok_toolkit/email/__init__.py +0 -0
- epok_toolkit/email/async_task.py +40 -0
- epok_toolkit/email/engine.py +69 -0
- epok_toolkit/email/templates.py +185 -0
- epok_toolkit-0.1.0.dist-info/METADATA +22 -0
- epok_toolkit-0.1.0.dist-info/RECORD +12 -0
- epok_toolkit-0.1.0.dist-info/WHEEL +5 -0
- epok_toolkit-0.1.0.dist-info/licenses/LICENSE +5 -0
- epok_toolkit-0.1.0.dist-info/top_level.txt +1 -0
epok_toolkit/__init__.py
ADDED
|
File without changes
|
epok_toolkit/apps.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
|
|
4
|
+
class EpokToolkitConfig(AppConfig):
|
|
5
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
6
|
+
name = 'epok_toolkit'
|
|
7
|
+
|
|
8
|
+
def ready(self):
|
|
9
|
+
# Importar tus settings por defecto
|
|
10
|
+
from . import default_settings
|
|
11
|
+
|
|
12
|
+
for setting in dir(default_settings):
|
|
13
|
+
if setting.isupper() and not hasattr(settings, setting):
|
|
14
|
+
setattr(settings, setting, getattr(default_settings, setting))
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import smtplib
|
|
5
|
+
from celery import shared_task, states
|
|
6
|
+
from celery.exceptions import Ignore
|
|
7
|
+
from .engine import EmailEngine
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
@shared_task(
|
|
12
|
+
bind=True,
|
|
13
|
+
autoretry_for=(smtplib.SMTPException, ConnectionError, TimeoutError),
|
|
14
|
+
retry_backoff=True, # 2, 4, 8, 16… s
|
|
15
|
+
retry_jitter=True, # +- aleatorio
|
|
16
|
+
max_retries=3,
|
|
17
|
+
ignore_result=True, # <-- ¡importante!
|
|
18
|
+
)
|
|
19
|
+
def send_email_async(self, template_key, context, recipient, attachments=None):
|
|
20
|
+
try:
|
|
21
|
+
engine = EmailEngine()
|
|
22
|
+
engine.send(
|
|
23
|
+
template_key=template_key,
|
|
24
|
+
context=context,
|
|
25
|
+
recipient=recipient,
|
|
26
|
+
attachments=attachments,
|
|
27
|
+
)
|
|
28
|
+
# No retornamos nada; evitamos que Celery/Redis serialice blobs.
|
|
29
|
+
except smtplib.SMTPSenderRefused as exc:
|
|
30
|
+
# 552 => el servidor rechaza tamaño. No vale la pena reintentar.
|
|
31
|
+
if exc.smtp_code == 552:
|
|
32
|
+
log.error("SMTP 552 – tamaño excedido; abortando reintentos: %s", exc)
|
|
33
|
+
# Marcamos la tarea como FAILURE, pero NO reintentamos.
|
|
34
|
+
self.update_state(state=states.FAILURE, meta=str(exc))
|
|
35
|
+
raise Ignore() # Detiene la cadena Celery sin más
|
|
36
|
+
# Otros códigos (550, 451, etc.) siguen flujo normal de retry
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
def send_email(template_key, context, recipient, attachments=None):
|
|
40
|
+
send_email_async.delay(template_key, context, recipient, attachments) # type: ignore
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
notifications/method/email_engine.py
|
|
3
|
+
Motor de envío de correos basado en plantillas del catálogo TEMPLATES.
|
|
4
|
+
La vista sólo indica qué plantilla usar, el contexto y el destinatario.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from django.core.mail import send_mail
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
|
|
11
|
+
from .templates import TEMPLATES, EmailTemplate, RenderedEmail
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EmailEngine:
|
|
15
|
+
"""
|
|
16
|
+
Ejemplo de uso:
|
|
17
|
+
|
|
18
|
+
engine = EmailEngine()
|
|
19
|
+
engine.send(
|
|
20
|
+
template_key="password_reset",
|
|
21
|
+
context={"name": "Fer", "reset_link": "https://app.congrats.mx/#/reset/abc"},
|
|
22
|
+
recipient="fer@example.com",
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, templates: Dict[str, EmailTemplate] = TEMPLATES, backend=send_mail):
|
|
27
|
+
self.templates = templates
|
|
28
|
+
self.backend = backend
|
|
29
|
+
|
|
30
|
+
# --------------------------------------------------------------------- #
|
|
31
|
+
# API pública
|
|
32
|
+
# --------------------------------------------------------------------- #
|
|
33
|
+
def send(
|
|
34
|
+
self,
|
|
35
|
+
template_key: "str", # TODO: PONER CLASE VALIDADOR DE TEMPLATE EVITAR INYECCION
|
|
36
|
+
context: Dict[str, str],
|
|
37
|
+
recipient: str,
|
|
38
|
+
attachments: Dict[str, bytes] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Envía un correo usando la plantilla indicada.
|
|
42
|
+
- `context` debe contener todas las llaves declaradas en `required_vars`.
|
|
43
|
+
- `attachments` puede ser un dict {filename: bytes} para adjuntar PDFs u otros ficheros.
|
|
44
|
+
"""
|
|
45
|
+
# Verificar plantilla
|
|
46
|
+
if template_key not in self.templates:
|
|
47
|
+
raise KeyError(f"Plantilla '{template_key}' no existe en TEMPLATES.") # TODO: PONER CUALES SI SON VALIDAS
|
|
48
|
+
|
|
49
|
+
# Renderizar cuerpo
|
|
50
|
+
rendered: RenderedEmail = self.templates[template_key].render(context)
|
|
51
|
+
|
|
52
|
+
# Crear e‑mail multipart (texto + HTML)
|
|
53
|
+
from django.core.mail import EmailMultiAlternatives
|
|
54
|
+
|
|
55
|
+
message = EmailMultiAlternatives(
|
|
56
|
+
subject=rendered.subject,
|
|
57
|
+
body=rendered.plain,
|
|
58
|
+
from_email=settings.EMAIL_HOST_USER,
|
|
59
|
+
to=[recipient],
|
|
60
|
+
)
|
|
61
|
+
message.attach_alternative(rendered.html, "text/html")
|
|
62
|
+
|
|
63
|
+
# Adjuntos opcionales
|
|
64
|
+
if attachments:
|
|
65
|
+
for name, data in attachments.items():
|
|
66
|
+
message.attach(name, data, "application/pdf")
|
|
67
|
+
|
|
68
|
+
# Enviar
|
|
69
|
+
message.send(fail_silently=False)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
notifications/method/email_templates.py
|
|
3
|
+
Catálogo central de plantillas de correo con estilo corporativo “morado minimalista”.
|
|
4
|
+
Cada plantilla declara las variables que necesita y se renderiza vía EmailTemplate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------#
|
|
12
|
+
# Shared purple‑minimalist wrapper
|
|
13
|
+
# ---------------------------------------------------------------------------#
|
|
14
|
+
def wrap_html(content: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Envuelve el bloque `content` en el layout HTML corporativo de Congrats.
|
|
17
|
+
"""
|
|
18
|
+
return f"""
|
|
19
|
+
<div style="font-family: Arial, Helvetica, sans-serif; background-color: #f9fafb; padding: 24px;">
|
|
20
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; margin: 0 auto; background: #ffffff; border-radius: 8px; overflow: hidden;">
|
|
21
|
+
<tr>
|
|
22
|
+
<td style="background: #4f46e5; padding: 20px 24px; text-align: center;">
|
|
23
|
+
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">Congrats 🎉</h1>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
|
|
27
|
+
<tr>
|
|
28
|
+
<td style="padding: 32px 24px;">
|
|
29
|
+
{content}
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
</div>
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------#
|
|
38
|
+
# Core dataclasses
|
|
39
|
+
# ---------------------------------------------------------------------------#
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class RenderedEmail:
|
|
42
|
+
subject: str
|
|
43
|
+
plain: str
|
|
44
|
+
html: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class EmailTemplate:
|
|
49
|
+
"""
|
|
50
|
+
subject – Cadena con placeholders, e.g. 'Hola {name}'
|
|
51
|
+
plain_body – Versión texto plano
|
|
52
|
+
html_body – HTML completo (usa wrap_html)
|
|
53
|
+
required_vars – Lista de variables obligatorias
|
|
54
|
+
"""
|
|
55
|
+
subject: str
|
|
56
|
+
plain_body: str
|
|
57
|
+
html_body: str
|
|
58
|
+
required_vars: List[str]
|
|
59
|
+
|
|
60
|
+
def render(self, context: Dict[str, str]) -> RenderedEmail:
|
|
61
|
+
missing = [v for v in self.required_vars if v not in context]
|
|
62
|
+
if missing:
|
|
63
|
+
raise ValueError(f"Faltan llaves en contexto: {missing}")
|
|
64
|
+
|
|
65
|
+
return RenderedEmail(
|
|
66
|
+
subject=self.subject.format(**context),
|
|
67
|
+
plain=self.plain_body.format(**context),
|
|
68
|
+
html=self.html_body.format(**context),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------#
|
|
73
|
+
# Template catalogue
|
|
74
|
+
# ---------------------------------------------------------------------------#
|
|
75
|
+
TEMPLATES: Dict[str, EmailTemplate] = {
|
|
76
|
+
"password_reset": EmailTemplate(
|
|
77
|
+
subject="🎉 Restablecimiento de Contraseña – Congrats 🎉",
|
|
78
|
+
plain_body=(
|
|
79
|
+
"¡Hola {name}! 🎉\n\n"
|
|
80
|
+
"Para poner tu fiesta de contraseñas en marcha, haz clic aquí:\n"
|
|
81
|
+
"{reset_link} 🎊\n\n"
|
|
82
|
+
"Si no fuiste tú quien pidió cambio, relájate y ignora este correo. 😉\n\n"
|
|
83
|
+
"¡Nos vemos en la pista de baile!\nEl equipo de Congrats 🥳"
|
|
84
|
+
),
|
|
85
|
+
html_body=wrap_html(
|
|
86
|
+
"<p style='font-size:16px;'>¡Hola <strong>{name}</strong>! 🎉</p>"
|
|
87
|
+
"<p style='font-size:16px;margin:24px 0;'>"
|
|
88
|
+
"Haz clic en el botón para restablecer tu contraseña y unirte a la celebración:</p>"
|
|
89
|
+
"<p style='text-align:center;margin:24px 0;'>"
|
|
90
|
+
"<a href='{reset_link}' style='display:inline-block;padding:12px 24px;font-size:16px;"
|
|
91
|
+
"color:#ffffff;background-color:#4f46e5;text-decoration:none;border-radius:5px;'>"
|
|
92
|
+
"🔒 Restablecer Contraseña 🎊</a>"
|
|
93
|
+
"</p>"
|
|
94
|
+
"<p style='font-size:16px;'>"
|
|
95
|
+
"Si eso no funciona, copia y pega este enlace en tu navegador:<br>"
|
|
96
|
+
"<span style='word-break:break-all;font-size:14px;'>{reset_link}</span>"
|
|
97
|
+
"</p>"
|
|
98
|
+
"<p style='font-size:16px;margin-top:24px;'>"
|
|
99
|
+
"Si no solicitaste esto, tranquilo, nada cambió. 😌<br><br>"
|
|
100
|
+
"¡A celebrar pronto!<br><em>El equipo de Congrats 🥳</em>"
|
|
101
|
+
"</p>"
|
|
102
|
+
),
|
|
103
|
+
required_vars=["name", "reset_link"],
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
"welcome": EmailTemplate(
|
|
107
|
+
subject="¡Bienvenido a Congrats, {name}! 🎉🥳",
|
|
108
|
+
plain_body=(
|
|
109
|
+
"¡Hola {name}! 🎈\n\n"
|
|
110
|
+
"Gracias por registrarte en Congrats. Prepárate para la mejor fiesta de eventos. 😎\n\n"
|
|
111
|
+
"¡Nos vemos pronto!\nEl equipo de Congrats 🥳"
|
|
112
|
+
),
|
|
113
|
+
html_body=wrap_html(
|
|
114
|
+
"<p style='font-size:16px;'>¡Hola <strong>{name}</strong>! 🎈</p>"
|
|
115
|
+
"<p style='font-size:16px;margin:24px 0;'>"
|
|
116
|
+
"Gracias por unirte a <strong>Congrats 🎉</strong>. Prepárate para la mejor fiesta de eventos. 😎"
|
|
117
|
+
"</p>"
|
|
118
|
+
"<p style='font-size:16px;margin-top:24px;'>"
|
|
119
|
+
"¡Nos vemos pronto!<br><em>El equipo de Congrats 🥳</em>"
|
|
120
|
+
"</p>"
|
|
121
|
+
),
|
|
122
|
+
required_vars=["name"],
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
"password_reset_success": EmailTemplate(
|
|
127
|
+
subject="🔑 Contraseña restablecida – Congrats 🥳",
|
|
128
|
+
plain_body=(
|
|
129
|
+
"¡Hola {name}! 🔑\n\n"
|
|
130
|
+
"Tu contraseña ya está lista para seguir la fiesta. 🎉\n\n"
|
|
131
|
+
"Si no fuiste tú, ponte alerta. 😉\n\n"
|
|
132
|
+
"Saludos festivos,\nEl equipo de Congrats 🥳"
|
|
133
|
+
),
|
|
134
|
+
html_body=wrap_html(
|
|
135
|
+
"<p style='font-size:16px;'>¡Hola <strong>{name}</strong>! 🔑</p>"
|
|
136
|
+
"<p style='font-size:16px;margin:24px 0;'>"
|
|
137
|
+
"Tu contraseña ha sido restablecida con éxito. Ahora vuelve a la pista de baile. 🎉"
|
|
138
|
+
"</p>"
|
|
139
|
+
"<p style='font-size:16px;margin-top:24px;'>"
|
|
140
|
+
"Si no fuiste tú, ignora o avísanos. 🤔<br><em>El equipo de Congrats 🥳</em>"
|
|
141
|
+
"</p>"
|
|
142
|
+
),
|
|
143
|
+
required_vars=["name"],
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
"ticket_created": EmailTemplate(
|
|
148
|
+
subject="🎫 ¡Tus tickets para {reunion_name} están listos! 🎉",
|
|
149
|
+
plain_body=(
|
|
150
|
+
"¡Hola {name}! 🎟️\n\n"
|
|
151
|
+
"Has comprado {tickets} tickets para “{reunion_name}”. ¡A vivir la experiencia! 🎊\n\n"
|
|
152
|
+
"¡Disfruta al máximo!\nEl equipo de Congrats 🥳"
|
|
153
|
+
),
|
|
154
|
+
html_body=wrap_html(
|
|
155
|
+
"<p style='font-size:16px;'>¡Hola <strong>{name}</strong>! 🎟️</p>"
|
|
156
|
+
"<p style='font-size:16px;margin:24px 0;'>"
|
|
157
|
+
"Tus {tickets} tickets para <strong>{reunion_name}</strong> están listos. ¡Nos vemos en la fiesta! 🎊"
|
|
158
|
+
"</p>"
|
|
159
|
+
"<p style='font-size:16px;margin-top:24px;'>"
|
|
160
|
+
"¡Que lo disfrutes!<br><em>El equipo de Congrats 🥳</em>"
|
|
161
|
+
"</p>"
|
|
162
|
+
),
|
|
163
|
+
required_vars=["name", "reunion_name", "tickets"],
|
|
164
|
+
),
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
"test_app_running": EmailTemplate(
|
|
168
|
+
subject="🚀 Test OK – Congrats 🎉",
|
|
169
|
+
plain_body=(
|
|
170
|
+
"¡Hola {name}! 🚀\n\n"
|
|
171
|
+
"Tu aplicación Congrats está activa y rockeando. 🤘\n\n"
|
|
172
|
+
"Sigue brillando,\nEl equipo de Congrats 🥳"
|
|
173
|
+
),
|
|
174
|
+
html_body=wrap_html(
|
|
175
|
+
"<p style='font-size:16px;'>¡Hola <strong>{name}</strong>! 🚀</p>"
|
|
176
|
+
"<p style='font-size:16px;margin:24px 0;'>"
|
|
177
|
+
"La prueba de funcionamiento pasó. Tu app está lista para la fiesta. 🎉"
|
|
178
|
+
"</p>"
|
|
179
|
+
"<p style='font-size:16px;margin-top:24px;'>"
|
|
180
|
+
"Sigue brillando.<br><em>El equipo de Congrats 🥳</em>"
|
|
181
|
+
"</p>"
|
|
182
|
+
),
|
|
183
|
+
required_vars=["name"],
|
|
184
|
+
),
|
|
185
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epok-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A toolkit for building Django applications with Celery support
|
|
5
|
+
Author-email: Fernando Leon Franco <fernanlee2131@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Framework :: Django
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: Django>=5.0.2
|
|
15
|
+
Requires-Dist: celery[redis]>=5.5.3
|
|
16
|
+
Requires-Dist: django-celery-beat>=2.6.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# EPOK Toolkit
|
|
20
|
+
|
|
21
|
+
This is a modular toolkit for Django projects including messaging, email, and PDF generation components. Built by Fer León.
|
|
22
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
epok_toolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
epok_toolkit/apps.py,sha256=O3q3CcucJOHjlYIS0VgbKsbtim2hpng_FxpKEG_MlWs,486
|
|
3
|
+
epok_toolkit/default_settings.py,sha256=SxRAoLm67uHhUHwz1-xeMaN6MRB8FiN0ZVlQZFDF27U,103
|
|
4
|
+
epok_toolkit/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
epok_toolkit/email/async_task.py,sha256=oC0WowWNUpTpXdxo6hJag5gWUaStxI6VBEArEKQxXko,1521
|
|
6
|
+
epok_toolkit/email/engine.py,sha256=IIifqRI9z76pHdrO5oSSZ25aP5txOTAgrj1JuVVPlMY,2387
|
|
7
|
+
epok_toolkit/email/templates.py,sha256=uO3gYn2iiGxcjxioaG066gGPts1m-3C1Gp7XatGgVOg,7578
|
|
8
|
+
epok_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=iLDbGXdLSIOT5OsxzHCvtmxHtonE21GiFlS3LNkug4A,128
|
|
9
|
+
epok_toolkit-0.1.0.dist-info/METADATA,sha256=sDgLv2prb5w5aJTKKR6OmQnHhl-qV40z5rDrJinUeHU,739
|
|
10
|
+
epok_toolkit-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
epok_toolkit-0.1.0.dist-info/top_level.txt,sha256=Wo72AqIFcfWwBGM5F5iGFw9PrO3WBnTSprFZIJk_pNg,13
|
|
12
|
+
epok_toolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
epok_toolkit
|