Mejorando el envío de correos usando templates
Hace algún tiempo en un artículo escribí sobre el envío de correos aqui explique un poco como se puede enviar correos de manera sencilla sin embargo esta solución se puede mejorar. Anteriormente tenía 3 métodos principales como se muestra a continuación
el método build_email encargado de construir la estructura del correo, el método send_email encargado de llamar al boto3 con sus parámetros y el método open_html método subrayado en rojo encargado de abrir un html y reemplazar variables que se habían declarado, esto funciona y cumple su objetivo sin embargo a continuación veremos a qué problemática nos podemos enfrentar a futuro
Problemática
antes de analizar los puntos mostraré la porción de código específico que se usaba en el método para reemplazar de manera manual una variable en un html
def open_html(name_client):
html_data: str = ""
with open("template.html", mode="r", encoding="utf-8") as data:
html_data = data.read().replace("\n", "")
html_data = html_data.replace("{{name_client}}", name_client)
return html_data
- Se tienen demasiadas limitaciones que pasa si se necesitan ciclos? validaciones? transformación de información? este tipo de soluciones se puede utilizar como herramienta para envío masivo a clientes , alrededor de esto existen algunas tendencias que marcan esta línea donde se muestra soluciones que tienen un nivel de personalización que requería complejidad y con la solución planteada sera muy difícil crecer
- Se tornara lento, pesado e ineficiente debido a que al no utilizarse ninguna mejora en el rendimiento a medida que se reemplace más información este proceso realizado manual va a tener un costo en función del tiempo bastante alto
- Será difícil construir un código que sea amigable y adaptable a los cambios que va exigiendo las transformaciones, por tanto cada cambio será costoso a medida que este se encargue de realizar mas operaciones
- Ya existen soluciones en le mercado que hacen este trabajo porque ignorarlas? como se aprecia en la Figura 2
Template Engine
Un template engine es un motor que permite combinar templates de diferentes formatos (YAML, XML, HTML) con modelos de datos para producir documentos, esto quiere decir que son estructuras que tienen sus propias etiquetas las cuales facilitan producir contenido dinámico, estos motores de template no son propios solamente en python, en javascript existen algunos muy conocidos entre esos está pug. Como se pudo apreciar en la Figura 2 en python tenemos varias opciones, si han trabajado con django, django tiene su propio motor de template el cual jinja es muy similar y será en este caso el que se usará
JINJA
Jinja (su nombre viene del nombre de un templo japonés, y en inglés la pronunciación es similar a template) es un motor de templates que contienen una gran cantidad de características que potenciará trabajar con contenido dinámico, además de ello es bastante eficiente casi 10x que django templates, con esta información pasare a la instalación y luego a mostrar un ejemplo, para instalarlo usare poetry
poetry add jinja2 pydantic
creare un folder dentro de mi proyecto llamado templates, y con ello le diré a jinja que busque en este folder todos los html, dentro de este folder creare dos archivos de la siguiente forma un archivo llamado base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jairo Andres</title>
</head>
<body>
<div class="center">
{% block card %}
{% endblock %}
</div>
</body>
</html>
y otro archivo llamado welcome.html
{% extends "base.html" %}
{% block card %}
<div>
<h3>Welcome {{user.name}}</h3>
<p>your information is:</p>
{% if user.show_information %}
<ul>
<li>Awards: {{user.awards}} </li>
<li>Matches: {{user.matches}}</li>
<li>Date join: {{user.date_joined.strftime('%Y-%m-%d')}}</li>
<li>Pals: {{user.pals | length}}</li>
</ul>
{%else%}
<p>{{user.name}} has not information</p>
{% endif %}
<a href="https://jairoandres.com" title="Go to Home">Go to home</a>
</div>
{% endblock %}
y a continuación mostraré el código en python para generar el contenido dinámico
from datetime import date
from typing import List
from pydantic import BaseModel
from jinja2 import (
Environment,
select_autoescape,
FileSystemLoader,
)
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
class User(BaseModel):
name: str
awards: int
matches: int
date_joined: date = date.today()
pals: List[str]
show_information: bool = False
def render(user: User):
template_result = env.get_template("welcome.html")
template_result = template_result.render(user=user)
with open("welcome_email.html", "wb") as file:
file.write(template_result.encode())
user = User(
name="Jane Doe", awards=3, matches=5, pals=["Drake", "Josh"], show_information=True
)
render(user)
ejecuto el codigo y obtengo el siguiente resultado
en el código anterior en los html se puede observar un etiquetado especial (se puede utilizar al final de los archivos extension .jinja) que pertenece propiamente a jinja en él se puede destacar la siguiente etiqueta
<div class="center">
{% block card %}
{% endblock %}
</div>
esta etiqueta anterior me permite realizar herencia entre diferentes templates para abstraer estructuras y poder reutilizar código, esto es una de las mayores ventajas que se puede tener ya que pueden existir muchos formatos con estructuras similares que cambien algunas cosas mínimas, adicional se puede ver en el segundo template que se pueden utilizar instrucciones if, y filtros ( | length)
{% if user.show_information %}
<ul>
<li>Awards: {{user.awards}} </li>
<li>Matches: {{user.matches}}</li>
<li>Date join: {{user.date_joined.strftime('%Y-%m-%d')}}</li>
<li>Pals: {{user.pals | length}}</li>
</ul>
{%else%}
<p>{{user.name}} has not information</p>
{% endif %}
esto también es propio de jinja aunque si se ve es código pythonico, con ello termina de potencializar la capacidad de procesar información de manera dinámica, se puede destacar del código anterior también que jinja permite cargar carpetas enteras con archivos que serán utilizados
from jinja2 import (
Environment,
select_autoescape,
FileSystemLoader,
)
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
esto permite después llamar un template por su nombre y renderizarlo de manera sencilla pasando como parámetros sus variables como se aprecia en la función render
def render(user: User):
template_result = env.get_template("welcome.html")
template_result = template_result.render(user=user)
with open("welcome_email.html", "wb") as file:
file.write(template_result.encode())
para finalizar el código anterior, el método render devuelve un str por tanto con el método encode permite escribir en un archivo llamado welcome_email.html que contiene todo el html después de que el resultado ha sido renderizado
Integración
Después de ver el ejemplo del funcionamiento en python con jinja re-utilizare el codigo del antiguo post para integrarlo con este motor y así poder generar el código dinámico como se ve a continuación
from datetime import date
from email.message import EmailMessage
from email.headerregistry import Address
from typing import List
import boto3
from io import BytesIO
from pydantic import BaseModel
from jinja2 import (
Environment,
select_autoescape,
FileSystemLoader,
)
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
class User(BaseModel):
name: str
awards: int
matches: int
date_joined: date = date.today()
pals: List[str]
show_information: bool = False
def render_html(user: User):
template_result = env.get_template("welcome.html")
template_result = template_result.render(user=user)
return template_result
def open_file_image():
file_image: BytesIO = None
with open("Wallpaper.png", mode="rb") as file:
file_image = file.read()
return file_image
def build_email(user: User):
email_message = EmailMessage()
email_message["Subject"] = "Welcome"
email_message["From"] = Address(
username="jairo", domain="eceleris.com", display_name="Jairo Andres"
)
email_message["To"] = Address(
username="jairo", domain="eceleris.com", display_name="Jairo Andres"
)
html_data: str = render_html(user)
email_message.add_alternative(html_data, subtype="html")
email_message.add_attachment(
open_file_image(), maintype="image", subtype="png", filename="Wallpaper.png"
)
send_email(email_message=email_message)
def send_email(email_message: EmailMessage):
client = boto3.client("sesv2")
response = client.send_email(
FromEmailAddress="jairo@eceleris.com",
Destination={
"ToAddresses": ["jairo@eceleris.com"],
},
Content={"Raw": {"Data": email_message.as_string()}},
)
user = User(
name="Jane Doe", awards=3, matches=5, pals=["Drake", "Josh"], show_information=True
)
build_email(user=user)
ejecuto el código y obtengo el siguiente resultado
en el código anterior agregue una función llamada render_html que es más corta de la función anterior, su trabajo consiste en renderizar un archivo utilizando todas las capacidades que tiene jinja
def render_html(user: User):
template_result = env.get_template("welcome.html")
template_result = template_result.render(user=user)
return template_result
aqui no es necesario guardarlo por que para enviarlo por AWS SES se utiliza la información en str, tal cual como la retorna el método render, finalmente se envía y en le buzón de mensajes se pudo apreciar el resultado mostrado en la Figura 4
Conclusiones
Utilizar un motor de template en lugar de conversiones de string para reemplazar información de manera dinámica en un archivo tiene un gran ventaja debido a que estos motores facilitan no solo reemplazar si no operar, utilizar filtros, validaciones. ciclos etc, para nuestro ejemplo utilice jinja ya que es uno de los motores que ha mostrado mayor rendimiento, es fácil de configurar , fácil de integrar y además su sintaxis es muy similar a la de django templates que es como si se escribiera código python. Por otro lado la utilidad de estos motores puede ir más allá, que pasaría si automatizamos un generador de código utilizando archivos YAML para proyectos repetitivos que son inicialmente muy similares? esto será para el siguiente post